adds bezel compensation settings

This commit is contained in:
cottongin 2026-01-27 21:40:52 -05:00
parent c2a966a6ea
commit 80c9e7a1d6
No known key found for this signature in database
GPG Key ID: 0ECC91FE4655C262
24 changed files with 428 additions and 155 deletions

View File

@ -2,6 +2,7 @@
1) Ability to clear all books and clear individual books from Recents.
2) Bookmarks
2a) crosspoint logo on firmware flashing screen
3) ability to add/remove books from lists on device.
4) quick menu
5) hide "system folders" from files view

View File

@ -621,15 +621,20 @@ void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char
const int pageHeight = getScreenHeight();
constexpr int buttonWidth = 106;
constexpr int buttonHeight = 40;
constexpr int buttonY = 40; // Distance from bottom
constexpr int textYOffset = 7; // Distance from top of button to text baseline
constexpr int buttonPositions[] = {25, 130, 245, 350};
constexpr int baseButtonY = 40; // Base distance from bottom
constexpr int textYOffset = 7; // Distance from top of button to text baseline
constexpr int baseButtonPositions[] = {25, 130, 245, 350};
const char* labels[] = {btn1, btn2, btn3, btn4};
// Apply bezel compensation (in portrait mode, bottom bezel affects Y position)
const int bezelBottom = getBezelOffsetBottom();
const int bezelLeft = getBezelOffsetLeft();
const int buttonY = baseButtonY + bezelBottom;
for (int i = 0; i < 4; i++) {
// Only draw if the label is non-empty
if (labels[i] != nullptr && labels[i][0] != '\0') {
const int x = buttonPositions[i];
const int x = baseButtonPositions[i] + bezelLeft;
fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
const int textWidth = getTextWidth(fontId, labels[i]);
@ -648,9 +653,14 @@ void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, cons
const int screenWidth = getScreenWidth();
constexpr int buttonWidth = 40; // Width on screen (height when rotated)
constexpr int buttonHeight = 80; // Height on screen (width when rotated)
constexpr int buttonX = 5; // Distance from right edge
constexpr int baseButtonX = 5; // Base distance from right edge
// Position for the button group - buttons share a border so they're adjacent
constexpr int topButtonY = 345; // Top button position
constexpr int baseTopButtonY = 345; // Base top button position
// Apply bezel compensation (in portrait mode)
const int bezelRight = getBezelOffsetRight();
const int buttonX = baseButtonX + bezelRight;
const int topButtonY = baseTopButtonY; // Y position doesn't need adjustment for side buttons
const char* labels[] = {topBtn, bottomBtn};
@ -979,31 +989,107 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp,
*x += glyph->advanceX;
}
// Helper to map physical bezel edge to logical edge based on orientation
// bezelEdge: 0=physical bottom, 1=physical top, 2=physical left, 3=physical right (in portrait)
// Returns: 0=logical bottom, 1=logical top, 2=logical left, 3=logical right
int mapPhysicalToLogicalEdge(int bezelEdge, GfxRenderer::Orientation orientation) {
switch (orientation) {
case GfxRenderer::Portrait:
return bezelEdge;
case GfxRenderer::LandscapeClockwise:
switch (bezelEdge) {
case 0: return 2; // Physical bottom -> logical left
case 1: return 3; // Physical top -> logical right
case 2: return 1; // Physical left -> logical top
case 3: return 0; // Physical right -> logical bottom
}
break;
case GfxRenderer::PortraitInverted:
switch (bezelEdge) {
case 0: return 1; // Physical bottom -> logical top
case 1: return 0; // Physical top -> logical bottom
case 2: return 3; // Physical left -> logical right
case 3: return 2; // Physical right -> logical left
}
break;
case GfxRenderer::LandscapeCounterClockwise:
switch (bezelEdge) {
case 0: return 3; // Physical bottom -> logical right
case 1: return 2; // Physical top -> logical left
case 2: return 0; // Physical left -> logical bottom
case 3: return 1; // Physical right -> logical top
}
break;
}
return bezelEdge;
}
int GfxRenderer::getViewableMarginTop() const {
int logicalEdge = mapPhysicalToLogicalEdge(bezelEdge, orientation);
return BASE_VIEWABLE_MARGIN_TOP + (logicalEdge == 1 ? bezelCompensation : 0);
}
int GfxRenderer::getViewableMarginRight() const {
int logicalEdge = mapPhysicalToLogicalEdge(bezelEdge, orientation);
return BASE_VIEWABLE_MARGIN_RIGHT + (logicalEdge == 3 ? bezelCompensation : 0);
}
int GfxRenderer::getViewableMarginBottom() const {
int logicalEdge = mapPhysicalToLogicalEdge(bezelEdge, orientation);
return BASE_VIEWABLE_MARGIN_BOTTOM + (logicalEdge == 0 ? bezelCompensation : 0);
}
int GfxRenderer::getViewableMarginLeft() const {
int logicalEdge = mapPhysicalToLogicalEdge(bezelEdge, orientation);
return BASE_VIEWABLE_MARGIN_LEFT + (logicalEdge == 2 ? bezelCompensation : 0);
}
int GfxRenderer::getBezelOffsetTop() const {
int logicalEdge = mapPhysicalToLogicalEdge(bezelEdge, orientation);
return (logicalEdge == 1) ? bezelCompensation : 0;
}
int GfxRenderer::getBezelOffsetRight() const {
int logicalEdge = mapPhysicalToLogicalEdge(bezelEdge, orientation);
return (logicalEdge == 3) ? bezelCompensation : 0;
}
int GfxRenderer::getBezelOffsetBottom() const {
int logicalEdge = mapPhysicalToLogicalEdge(bezelEdge, orientation);
return (logicalEdge == 0) ? bezelCompensation : 0;
}
int GfxRenderer::getBezelOffsetLeft() const {
int logicalEdge = mapPhysicalToLogicalEdge(bezelEdge, orientation);
return (logicalEdge == 2) ? bezelCompensation : 0;
}
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
// Get base margins rotated for current orientation, with bezel compensation applied
switch (orientation) {
case Portrait:
*outTop = VIEWABLE_MARGIN_TOP;
*outRight = VIEWABLE_MARGIN_RIGHT;
*outBottom = VIEWABLE_MARGIN_BOTTOM;
*outLeft = VIEWABLE_MARGIN_LEFT;
*outTop = getViewableMarginTop();
*outRight = getViewableMarginRight();
*outBottom = getViewableMarginBottom();
*outLeft = getViewableMarginLeft();
break;
case LandscapeClockwise:
*outTop = VIEWABLE_MARGIN_LEFT;
*outRight = VIEWABLE_MARGIN_TOP;
*outBottom = VIEWABLE_MARGIN_RIGHT;
*outLeft = VIEWABLE_MARGIN_BOTTOM;
*outTop = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
break;
case PortraitInverted:
*outTop = VIEWABLE_MARGIN_BOTTOM;
*outRight = VIEWABLE_MARGIN_LEFT;
*outBottom = VIEWABLE_MARGIN_TOP;
*outLeft = VIEWABLE_MARGIN_RIGHT;
*outTop = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
break;
case LandscapeCounterClockwise:
*outTop = VIEWABLE_MARGIN_RIGHT;
*outRight = VIEWABLE_MARGIN_BOTTOM;
*outBottom = VIEWABLE_MARGIN_LEFT;
*outLeft = VIEWABLE_MARGIN_TOP;
*outTop = BASE_VIEWABLE_MARGIN_RIGHT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 1 ? bezelCompensation : 0);
*outRight = BASE_VIEWABLE_MARGIN_BOTTOM + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 3 ? bezelCompensation : 0);
*outBottom = BASE_VIEWABLE_MARGIN_LEFT + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 0 ? bezelCompensation : 0);
*outLeft = BASE_VIEWABLE_MARGIN_TOP + (mapPhysicalToLogicalEdge(bezelEdge, orientation) == 2 ? bezelCompensation : 0);
break;
}
}

View File

@ -28,9 +28,17 @@ class GfxRenderer {
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE,
"BW buffer chunking does not line up with display buffer size");
// Base viewable margins (hardware-specific, before bezel compensation)
static constexpr int BASE_VIEWABLE_MARGIN_TOP = 9;
static constexpr int BASE_VIEWABLE_MARGIN_RIGHT = 3;
static constexpr int BASE_VIEWABLE_MARGIN_BOTTOM = 3;
static constexpr int BASE_VIEWABLE_MARGIN_LEFT = 3;
EInkDisplay& einkDisplay;
RenderMode renderMode;
Orientation orientation;
int bezelCompensation = 0; // Pixels to add for bezel defect compensation
int bezelEdge = 0; // Which physical edge (0=bottom, 1=top, 2=left, 3=right in portrait)
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
@ -42,10 +50,24 @@ class GfxRenderer {
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
~GfxRenderer() { freeBwBufferChunks(); }
static constexpr int VIEWABLE_MARGIN_TOP = 9;
static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
static constexpr int VIEWABLE_MARGIN_BOTTOM = 3;
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
// Viewable margins (includes bezel compensation applied to the configured edge)
int getViewableMarginTop() const;
int getViewableMarginRight() const;
int getViewableMarginBottom() const;
int getViewableMarginLeft() const;
// Bezel compensation configuration
void setBezelCompensation(int amount, int edge) {
bezelCompensation = amount;
bezelEdge = edge;
}
// Get bezel offset for each logical edge (0 if not the compensated edge)
// Use these to add bezel compensation to hardcoded margins in UI screens
int getBezelOffsetTop() const;
int getBezelOffsetRight() const;
int getBezelOffsetBottom() const;
int getBezelOffsetLeft() const;
// Setup
void insertFont(int fontId, EpdFontFamily font);

View File

@ -23,7 +23,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 27; // 26 + displayContrast
constexpr uint8_t SETTINGS_COUNT = 29; // 28 + bezelCompensationEdge
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace
@ -68,6 +68,10 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, sleepScreenCoverFilter);
// System-wide display contrast
serialization::writePod(outputFile, displayContrast);
// Bezel compensation for physical screen edge defects
serialization::writePod(outputFile, bezelCompensation);
// Which physical edge needs bezel compensation
serialization::writePod(outputFile, bezelCompensationEdge);
// New fields added at end for backward compatibility
outputFile.close();
@ -172,6 +176,12 @@ bool CrossPointSettings::loadFromFile() {
// System-wide display contrast (0 = normal, 1 = high)
serialization::readPod(inputFile, displayContrast);
if (++settingsRead >= fileSettingsCount) break;
// Bezel compensation for physical screen edge defects (0-10px)
serialization::readPod(inputFile, bezelCompensation);
if (++settingsRead >= fileSettingsCount) break;
// Which physical edge needs bezel compensation
readAndValidate(inputFile, bezelCompensationEdge, BEZEL_EDGE_COUNT);
if (++settingsRead >= fileSettingsCount) break;
// New fields added at end for backward compatibility
} while (false);

View File

@ -91,6 +91,10 @@ class CrossPointSettings {
// Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
// Bezel compensation edge (which physical edge has the defect)
// These refer to physical edges in portrait orientation
enum BEZEL_EDGE { BEZEL_BOTTOM = 0, BEZEL_TOP = 1, BEZEL_LEFT = 2, BEZEL_RIGHT = 3, BEZEL_EDGE_COUNT };
// Sleep screen settings
uint8_t sleepScreen = DARK;
// Sleep screen cover mode settings
@ -135,6 +139,11 @@ class CrossPointSettings {
uint8_t longPressChapterSkip = 1;
// System-wide display contrast (0 = normal, 1 = high)
uint8_t displayContrast = 0;
// Bezel compensation - extra margin for physical screen edge defects (0-10px)
// Applied to the physical edge specified by bezelCompensationEdge, rotates with orientation
uint8_t bezelCompensation = 0;
// Which physical edge needs compensation (in portrait orientation)
uint8_t bezelCompensationEdge = BEZEL_BOTTOM;
// Pinned list name (empty = none pinned)
char pinnedListName[64] = "";

View File

@ -7,6 +7,7 @@
#include <string>
#include "Battery.h"
#include "CrossPointSettings.h"
#include "fontIds.h"
void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top,

View File

@ -11,10 +11,15 @@ void BootActivity::onEnter() {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int centerY = (pageHeight - bezelTop - bezelBottom) / 2 + bezelTop;
renderer.clearScreen();
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING");
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, centerY - 64, 128, 128);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 70, "CrossPoint", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, centerY + 95, "BOOTING");
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30 - bezelBottom, CROSSPOINT_VERSION);
renderer.displayBuffer();
}

View File

@ -139,10 +139,15 @@ void SleepActivity::renderDefaultSleepScreen() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int centerY = (pageHeight - bezelTop - bezelBottom) / 2 + bezelTop;
renderer.clearScreen();
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING");
renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, centerY - 64, 128, 128);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 70, "CrossPoint", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, centerY + 95, "SLEEPING");
// Make sleep screen dark unless light is selected in settings
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) {

View File

@ -172,10 +172,17 @@ void OpdsBookBrowserActivity::render() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD);
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int centerY = (pageHeight - bezelTop - bezelBottom) / 2 + bezelTop;
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "OPDS Browser", true, EpdFontFamily::BOLD);
if (state == BrowserState::CHECK_WIFI) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, centerY, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
@ -183,7 +190,7 @@ void OpdsBookBrowserActivity::render() const {
}
if (state == BrowserState::LOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, centerY, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
@ -191,8 +198,8 @@ void OpdsBookBrowserActivity::render() const {
}
if (state == BrowserState::ERROR) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Error:");
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 20, "Error:");
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 10, errorMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "Retry", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
@ -200,13 +207,13 @@ void OpdsBookBrowserActivity::render() const {
}
if (state == BrowserState::DOWNLOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, "Downloading...");
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, statusMessage.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 40, "Downloading...");
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 10, statusMessage.c_str());
if (downloadTotal > 0) {
const int barWidth = pageWidth - 100;
const int barWidth = pageWidth - 100 - bezelLeft - bezelRight;
constexpr int barHeight = 20;
constexpr int barX = 50;
const int barY = pageHeight / 2 + 20;
const int barX = 50 + bezelLeft;
const int barY = centerY + 20;
ScreenComponents::drawProgressBar(renderer, barX, barY, barWidth, barHeight, downloadProgress, downloadTotal);
}
renderer.displayBuffer();
@ -223,13 +230,13 @@ void OpdsBookBrowserActivity::render() const {
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (entries.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "No entries found");
renderer.drawCenteredText(UI_10_FONT_ID, centerY, "No entries found");
renderer.displayBuffer();
return;
}
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
for (size_t i = pageStartIndex; i < entries.size() && i < static_cast<size_t>(pageStartIndex + PAGE_ITEMS); i++) {
const auto& entry = entries[i];
@ -246,8 +253,8 @@ void OpdsBookBrowserActivity::render() const {
}
}
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40);
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(),
auto item = renderer.truncatedText(UI_10_FONT_ID, displayText.c_str(), renderer.getScreenWidth() - 40 - bezelLeft - bezelRight);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, 60 + bezelTop + (i % PAGE_ITEMS) * 30, item.c_str(),
i != static_cast<size_t>(selectorIndex));
}

View File

@ -343,9 +343,12 @@ void HomeActivity::render() {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
constexpr int margin = 20;
constexpr int bottomMargin = 60;
constexpr int bookY = 30;
// Base margins plus bezel compensation
const int margin = 20 + renderer.getBezelOffsetLeft();
const int rightMargin = 20 + renderer.getBezelOffsetRight();
const int bottomMargin = 60 + renderer.getBezelOffsetBottom();
const int topMargin = renderer.getBezelOffsetTop();
const int bookY = 30 + topMargin;
constexpr int elementSpacing = 15;
// --- Calculate layout from bottom up ---
@ -366,7 +369,7 @@ void HomeActivity::render() {
fullWidthItems.insert(fullWidthItems.begin(), "OPDS Browser");
}
const int menuTileWidth = pageWidth - 2 * margin;
const int menuTileWidth = pageWidth - margin - rightMargin;
constexpr int menuTileHeight = 45;
constexpr int menuSpacing = 8;
const int halfTileWidth = (menuTileWidth - menuSpacing) / 2; // Account for spacing between halves
@ -375,10 +378,10 @@ void HomeActivity::render() {
menuTileHeight + static_cast<int>(fullWidthItems.size()) * (menuTileHeight + menuSpacing);
// Anchor menu to bottom of screen
const int menuStartY = pageHeight - bottomMargin - totalMenuHeight - margin;
const int menuStartY = pageHeight - bottomMargin - totalMenuHeight;
// Calculate book card dimensions - larger, filling available space
const int bookWidth = pageWidth - 2 * margin;
const int bookWidth = pageWidth - margin - rightMargin;
// Card extends to just above menu
const int bookCardBottomY = menuStartY - elementSpacing;
const int bookHeight = bookCardBottomY - bookY;
@ -721,7 +724,7 @@ void HomeActivity::render() {
for (size_t i = 0; i < fullWidthItems.size(); ++i) {
// Index offset: base + 2 (for Lists and My Library) + i
const int overallIndex = baseMenuIndex + 2 + static_cast<int>(i);
constexpr int tileX = margin;
const int tileX = margin;
const int tileY = firstRowY + menuTileHeight + menuSpacing + static_cast<int>(i) * (menuTileHeight + menuSpacing);
const bool selected = selectorIndex == overallIndex;

View File

@ -12,15 +12,15 @@
#include "util/StringUtils.h"
namespace {
// Layout constants (matching MyLibraryActivity's Recent tab)
constexpr int HEADER_Y = 15;
constexpr int CONTENT_START_Y = 60;
// Base layout constants (bezel offsets added at render time)
constexpr int BASE_HEADER_Y = 15;
constexpr int BASE_CONTENT_START_Y = 60;
constexpr int LINE_HEIGHT = 65; // Two-line items (title + author)
constexpr int LEFT_MARGIN = 20;
constexpr int RIGHT_MARGIN = 40;
constexpr int BASE_LEFT_MARGIN = 20;
constexpr int BASE_RIGHT_MARGIN = 40;
constexpr int MICRO_THUMB_WIDTH = 45;
constexpr int MICRO_THUMB_HEIGHT = 60;
constexpr int THUMB_RIGHT_MARGIN = 50;
constexpr int BASE_THUMB_RIGHT_MARGIN = 50;
// Timing thresholds
constexpr int SKIP_PAGE_MS = 700;
@ -41,7 +41,9 @@ std::string getMicroThumbPathForBook(const std::string& bookPath) {
int ListViewActivity::getPageItems() const {
const int screenHeight = renderer.getScreenHeight();
const int bottomBarHeight = 60;
const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight;
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int availableHeight = screenHeight - (BASE_CONTENT_START_Y + bezelTop) - bottomBarHeight - bezelBottom;
int items = availableHeight / LINE_HEIGHT;
if (items < 1) {
items = 1;
@ -172,6 +174,16 @@ void ListViewActivity::render() const {
const int pageItems = getPageItems();
const int bookCount = static_cast<int>(bookList.books.size());
// Calculate bezel-adjusted margins
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int HEADER_Y = BASE_HEADER_Y + bezelTop;
const int CONTENT_START_Y = BASE_CONTENT_START_Y + bezelTop;
const int LEFT_MARGIN = BASE_LEFT_MARGIN + bezelLeft;
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
const int THUMB_RIGHT_MARGIN = BASE_THUMB_RIGHT_MARGIN + bezelRight;
// Draw header with list name
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, listName.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN);
renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, HEADER_Y, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
@ -190,7 +202,7 @@ void ListViewActivity::render() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN,
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
LINE_HEIGHT);
// Calculate available text width

View File

@ -31,16 +31,16 @@ void MyLibraryActivity::clearThumbExistsCache() {
}
namespace {
// Layout constants
constexpr int TAB_BAR_Y = 15;
constexpr int CONTENT_START_Y = 60;
// Base layout constants (bezel offsets added at render time)
constexpr int BASE_TAB_BAR_Y = 15;
constexpr int BASE_CONTENT_START_Y = 60;
constexpr int LINE_HEIGHT = 30;
constexpr int RECENTS_LINE_HEIGHT = 65; // Increased for two-line items
constexpr int LEFT_MARGIN = 20;
constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator
constexpr int BASE_LEFT_MARGIN = 20;
constexpr int BASE_RIGHT_MARGIN = 40; // Extra space for scroll indicator
constexpr int MICRO_THUMB_WIDTH = 45;
constexpr int MICRO_THUMB_HEIGHT = 60;
constexpr int THUMB_RIGHT_MARGIN = 50; // Space from right edge for thumbnail
constexpr int BASE_THUMB_RIGHT_MARGIN = 50; // Space from right edge for thumbnail
// Helper function to get the micro-thumb path for a book based on its file path
std::string getMicroThumbPathForBook(const std::string& bookPath) {
@ -74,7 +74,9 @@ void sortFileList(std::vector<std::string>& strs) {
int MyLibraryActivity::getPageItems() const {
const int screenHeight = renderer.getScreenHeight();
const int bottomBarHeight = 60; // Space for button hints
const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight;
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int availableHeight = screenHeight - (BASE_CONTENT_START_Y + bezelTop) - bottomBarHeight - bezelBottom;
// Recent tab uses taller items (title + author), Lists and Files use single-line items
const int lineHeight = (currentTab == Tab::Recent) ? RECENTS_LINE_HEIGHT : LINE_HEIGHT;
int items = availableHeight / lineHeight;
@ -700,6 +702,12 @@ void MyLibraryActivity::render() const {
return;
}
// Calculate bezel-adjusted margins
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int TAB_BAR_Y = BASE_TAB_BAR_Y + bezelTop;
const int CONTENT_START_Y = BASE_CONTENT_START_Y + bezelTop;
// Normal state - draw library view
// Draw tab bar
std::vector<TabInfo> tabs = {{"Recent", currentTab == Tab::Recent},
@ -718,7 +726,7 @@ void MyLibraryActivity::render() const {
// Draw scroll indicator
const int screenHeight = renderer.getScreenHeight();
const int contentHeight = screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar
const int contentHeight = screenHeight - CONTENT_START_Y - 60 - bezelBottom; // 60 for bottom bar
ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight);
// Draw side button hints (up/down navigation on right side)
@ -737,6 +745,15 @@ void MyLibraryActivity::renderRecentTab() const {
const int pageItems = getPageItems();
const int bookCount = static_cast<int>(recentBooks.size());
// Calculate bezel-adjusted margins
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int CONTENT_START_Y = BASE_CONTENT_START_Y + bezelTop;
const int LEFT_MARGIN = BASE_LEFT_MARGIN + bezelLeft;
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
const int THUMB_RIGHT_MARGIN = BASE_THUMB_RIGHT_MARGIN + bezelRight;
if (bookCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books");
return;
@ -745,8 +762,8 @@ void MyLibraryActivity::renderRecentTab() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * RECENTS_LINE_HEIGHT - 2,
pageWidth - RIGHT_MARGIN, RECENTS_LINE_HEIGHT);
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * RECENTS_LINE_HEIGHT - 2,
pageWidth - RIGHT_MARGIN - bezelLeft, RECENTS_LINE_HEIGHT);
// Calculate available text width (leaving space for thumbnail on the right)
const int textMaxWidth = pageWidth - LEFT_MARGIN - RIGHT_MARGIN - MICRO_THUMB_WIDTH - 10;
@ -897,6 +914,14 @@ void MyLibraryActivity::renderListsTab() const {
const int pageItems = getPageItems();
const int listCount = static_cast<int>(lists.size());
// Calculate bezel-adjusted margins
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int CONTENT_START_Y = BASE_CONTENT_START_Y + bezelTop;
const int LEFT_MARGIN = BASE_LEFT_MARGIN + bezelLeft;
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
if (listCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No lists found");
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + LINE_HEIGHT, "Create lists in Companion App");
@ -906,7 +931,7 @@ void MyLibraryActivity::renderListsTab() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN,
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
LINE_HEIGHT);
// Draw items
@ -927,6 +952,14 @@ void MyLibraryActivity::renderFilesTab() const {
const int pageItems = getPageItems();
const int fileCount = static_cast<int>(files.size());
// Calculate bezel-adjusted margins
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int CONTENT_START_Y = BASE_CONTENT_START_Y + bezelTop;
const int LEFT_MARGIN = BASE_LEFT_MARGIN + bezelLeft;
const int RIGHT_MARGIN = BASE_RIGHT_MARGIN + bezelRight;
if (fileCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No books found");
return;
@ -935,7 +968,7 @@ void MyLibraryActivity::renderFilesTab() const {
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
// Draw selection highlight
renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN,
renderer.fillRect(bezelLeft, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN - bezelLeft,
LINE_HEIGHT);
// Draw items
@ -950,12 +983,17 @@ void MyLibraryActivity::renderActionMenu() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
// Title
renderer.drawCenteredText(UI_12_FONT_ID, 20, "Book Actions", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 20 + bezelTop, "Book Actions", true, EpdFontFamily::BOLD);
// Show filename
const int filenameY = 70;
auto truncatedName = renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40);
const int filenameY = 70 + bezelTop;
auto truncatedName = renderer.truncatedText(UI_10_FONT_ID, actionTargetName.c_str(), pageWidth - 40 - bezelLeft - bezelRight);
renderer.drawCenteredText(UI_10_FONT_ID, filenameY, truncatedName.c_str());
// Menu options - 4 for Recent tab, 2 for Files tab

View File

@ -211,10 +211,13 @@ void CalibreConnectActivity::render() const {
renderer.clearScreen();
const auto pageHeight = renderer.getScreenHeight();
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int centerY = (pageHeight - bezelTop - bezelBottom) / 2 + bezelTop;
if (state == CalibreConnectState::SERVER_STARTING) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Calibre...", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, centerY - 20, "Starting Calibre...", true, EpdFontFamily::BOLD);
} else if (state == CalibreConnectState::ERROR) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Calibre setup failed", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, centerY - 20, "Calibre setup failed", true, EpdFontFamily::BOLD);
}
renderer.displayBuffer();
}
@ -224,7 +227,8 @@ void CalibreConnectActivity::renderServerRunning() const {
constexpr int SMALL_SPACING = 20;
constexpr int SECTION_SPACING = 40;
constexpr int TOP_PADDING = 14;
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Connect to Calibre", true, EpdFontFamily::BOLD);
const int bezelTop = renderer.getBezelOffsetTop();
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Connect to Calibre", true, EpdFontFamily::BOLD);
int y = 55 + TOP_PADDING;
renderer.drawCenteredText(UI_10_FONT_ID, y, "Network", true, EpdFontFamily::BOLD);

View File

@ -460,7 +460,10 @@ void CrossPointWebServerActivity::render() const {
} else if (state == WebServerActivityState::AP_STARTING) {
renderer.clearScreen();
const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, EpdFontFamily::BOLD);
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int centerY = (pageHeight - bezelTop - bezelBottom) / 2 + bezelTop;
renderer.drawCenteredText(UI_12_FONT_ID, centerY - 20, "Starting Hotspot...", true, EpdFontFamily::BOLD);
renderer.displayBuffer();
}
}

View File

@ -105,15 +105,21 @@ void NetworkModeSelectionActivity::render() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "File Transfer", true, EpdFontFamily::BOLD);
// Draw subtitle
renderer.drawCenteredText(UI_10_FONT_ID, 50, "How would you like to connect?");
renderer.drawCenteredText(UI_10_FONT_ID, 50 + bezelTop, "How would you like to connect?");
// Draw menu items centered on screen
constexpr int itemHeight = 50; // Height for each menu item (including description)
const int startY = (pageHeight - (MENU_ITEM_COUNT * itemHeight)) / 2 + 10;
const int startY = (pageHeight - bezelTop - bezelBottom - (MENU_ITEM_COUNT * itemHeight)) / 2 + bezelTop + 10;
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
const int itemY = startY + i * itemHeight;
@ -121,13 +127,13 @@ void NetworkModeSelectionActivity::render() const {
// Draw selection highlight (black fill) for selected item
if (isSelected) {
renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6);
renderer.fillRect(20 + bezelLeft, itemY - 2, pageWidth - 40 - bezelLeft - bezelRight, itemHeight - 6);
}
// Draw text: black=false (white text) when selected (on black background)
// black=true (black text) when not selected (on white background)
renderer.drawText(UI_10_FONT_ID, 30, itemY, MENU_ITEMS[i], /*black=*/!isSelected);
renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected);
renderer.drawText(UI_10_FONT_ID, 30 + bezelLeft, itemY, MENU_ITEMS[i], /*black=*/!isSelected);
renderer.drawText(SMALL_FONT_ID, 30 + bezelLeft, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected);
}
// Draw help text at bottom

View File

@ -513,8 +513,14 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Networks", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "WiFi Networks", true, EpdFontFamily::BOLD);
if (networks.empty()) {
// No networks found or scan failed
@ -524,9 +530,9 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again");
} else {
// Calculate how many networks we can display
constexpr int startY = 60;
const int startY = 60 + bezelTop;
constexpr int lineHeight = 25;
const int maxVisibleNetworks = (pageHeight - startY - 40) / lineHeight;
const int maxVisibleNetworks = (pageHeight - startY - 40 - bezelBottom) / lineHeight;
// Calculate scroll offset to keep selected item visible
int scrollOffset = 0;
@ -542,7 +548,7 @@ void WifiSelectionActivity::renderNetworkList() const {
// Draw selection indicator
if (static_cast<int>(i) == selectedNetworkIndex) {
renderer.drawText(UI_10_FONT_ID, 5, networkY, ">");
renderer.drawText(UI_10_FONT_ID, 5 + bezelLeft, networkY, ">");
}
// Draw network name (truncate if too long)
@ -550,42 +556,42 @@ void WifiSelectionActivity::renderNetworkList() const {
if (displayName.length() > 16) {
displayName.replace(13, displayName.length() - 13, "...");
}
renderer.drawText(UI_10_FONT_ID, 20, networkY, displayName.c_str());
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, networkY, displayName.c_str());
// Draw signal strength indicator
std::string signalStr = getSignalStrengthIndicator(network.rssi);
renderer.drawText(UI_10_FONT_ID, pageWidth - 90, networkY, signalStr.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 90 - bezelRight, networkY, signalStr.c_str());
// Draw saved indicator (checkmark) for networks with saved passwords
if (network.hasSavedPassword) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 50, networkY, "+");
renderer.drawText(UI_10_FONT_ID, pageWidth - 50 - bezelRight, networkY, "+");
}
// Draw lock icon for encrypted networks
if (network.isEncrypted) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 30, networkY, "*");
renderer.drawText(UI_10_FONT_ID, pageWidth - 30 - bezelRight, networkY, "*");
}
}
// Draw scroll indicators if needed
if (scrollOffset > 0) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY - 10, "^");
renderer.drawText(SMALL_FONT_ID, pageWidth - 15 - bezelRight, startY - 10, "^");
}
if (scrollOffset + maxVisibleNetworks < static_cast<int>(networks.size())) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY + maxVisibleNetworks * lineHeight, "v");
renderer.drawText(SMALL_FONT_ID, pageWidth - 15 - bezelRight, startY + maxVisibleNetworks * lineHeight, "v");
}
// Show network count
char countStr[32];
snprintf(countStr, sizeof(countStr), "%zu networks found", networks.size());
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr);
renderer.drawText(SMALL_FONT_ID, 20 + bezelLeft, pageHeight - 90 - bezelBottom, countStr);
}
// Show MAC address above the network count and legend
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
renderer.drawText(SMALL_FONT_ID, 20 + bezelLeft, pageHeight - 105 - bezelBottom, cachedMacAddress.c_str());
// Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved");
renderer.drawText(SMALL_FONT_ID, 20 + bezelLeft, pageHeight - 75 - bezelBottom, "* = Encrypted | + = Saved");
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}

View File

@ -166,28 +166,33 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
const int pageItems = getPageItems();
const int totalItems = getTotalItems();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const std::string title =
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD);
renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40 - bezelLeft - bezelRight, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, title.c_str(), true, EpdFontFamily::BOLD);
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) {
const int displayY = 60 + (itemIndex % pageItems) * 30;
const int displayY = 60 + bezelTop + (itemIndex % pageItems) * 30;
const bool isSelected = (itemIndex == selectorIndex);
if (isSyncItem(itemIndex)) {
// Draw sync option (at top or bottom)
renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, displayY, ">> Sync Progress", !isSelected);
} else {
// Draw TOC item (account for top sync offset)
const int tocIndex = tocIndexFromItemIndex(itemIndex);
auto item = epub->getTocItem(tocIndex);
const int indentSize = 20 + (item.level - 1) * 15;
const int indentSize = 20 + bezelLeft + (item.level - 1) * 15;
const std::string chapterName =
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize);
renderer.drawText(UI_10_FONT_ID, indentSize, 60 + (tocIndex % pageItems) * 30, chapterName.c_str(),
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - bezelLeft - bezelRight - indentSize + 20 + bezelLeft);
renderer.drawText(UI_10_FONT_ID, indentSize, 60 + bezelTop + (tocIndex % pageItems) * 30, chapterName.c_str(),
tocIndex != selectorIndex);
}
}

View File

@ -153,21 +153,26 @@ void CalibreSettingsActivity::render() {
const auto pageWidth = renderer.getScreenWidth();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "OPDS Browser", true, EpdFontFamily::BOLD);
// Draw info text about Calibre
renderer.drawCenteredText(UI_10_FONT_ID, 40, "For Calibre, add /opds to your URL");
renderer.drawCenteredText(UI_10_FONT_ID, 40 + bezelTop, "For Calibre, add /opds to your URL");
// Draw selection highlight
renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30);
renderer.fillRect(bezelLeft, 70 + bezelTop + selectedIndex * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
// Draw menu items
for (int i = 0; i < MENU_ITEMS; i++) {
const int settingY = 70 + i * 30;
const int settingY = 70 + bezelTop + i * 30;
const bool isSelected = (i == selectedIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, settingY, menuNames[i], !isSelected);
// Draw status for each setting
const char* status = "[Not Set]";
@ -179,7 +184,7 @@ void CalibreSettingsActivity::render() {
status = (strlen(SETTINGS.opdsPassword) > 0) ? "[Set]" : "[Not Set]";
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - bezelRight - width, settingY, status, !isSelected);
}
// Draw button hints

View File

@ -124,6 +124,11 @@ void CategorySettingsActivity::toggleCurrentSetting() {
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
// Handle side effects for specific settings
if (setting.valuePtr == &CrossPointSettings::bezelCompensationEdge) {
renderer.setBezelCompensation(SETTINGS.bezelCompensation, SETTINGS.bezelCompensationEdge);
}
} else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
const int8_t currentValue = SETTINGS.*(setting.valuePtr);
if (currentValue + setting.valueRange.step > setting.valueRange.max) {
@ -131,6 +136,11 @@ void CategorySettingsActivity::toggleCurrentSetting() {
} else {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
}
// Handle side effects for specific settings
if (setting.valuePtr == &CrossPointSettings::bezelCompensation) {
renderer.setBezelCompensation(SETTINGS.bezelCompensation, SETTINGS.bezelCompensationEdge);
}
} else if (setting.type == SettingType::ACTION) {
if (strcmp(setting.name, "Calibre Settings") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -182,10 +192,16 @@ void CategorySettingsActivity::render() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD);
// Bezel compensation offsets
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int bezelBottom = renderer.getBezelOffsetBottom();
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, categoryName, true, EpdFontFamily::BOLD);
// Draw selection highlight
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + selectedSettingIndex * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
// Draw only visible settings
int visibleIndex = 0;
@ -194,11 +210,11 @@ void CategorySettingsActivity::render() const {
continue; // Skip hidden settings
}
const int settingY = 60 + visibleIndex * 30; // 30 pixels between settings
const int settingY = 60 + bezelTop + visibleIndex * 30; // 30 pixels between settings
const bool isSelected = (visibleIndex == selectedSettingIndex);
// Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, settingY, settingsList[i].name, !isSelected);
// Draw value based on setting type
std::string valueText;
@ -215,14 +231,14 @@ void CategorySettingsActivity::render() const {
}
if (!valueText.empty()) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - bezelRight - width, settingY, valueText.c_str(), !isSelected);
}
visibleIndex++;
}
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - bezelRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60 - bezelBottom, CROSSPOINT_VERSION);
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@ -58,15 +58,20 @@ void ClearCacheActivity::displayTaskLoop() {
void ClearCacheActivity::render() {
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int centerY = (pageHeight - bezelTop - bezelBottom) / 2 + bezelTop;
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Clear Cache", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Clear Cache", true, EpdFontFamily::BOLD);
if (state == WARNING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 30, "All reading progress will be lost!", true,
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 60, "This will clear all cached book data.", true);
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 30, "All reading progress will be lost!", true,
EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Books will need to be re-indexed", true);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 30, "when opened again.", true);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 10, "Books will need to be re-indexed", true);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 30, "when opened again.", true);
const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -75,18 +80,18 @@ void ClearCacheActivity::render() {
}
if (state == CLEARING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Clearing cache...", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY, "Clearing cache...", true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Cache Cleared", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 20, "Cache Cleared", true, EpdFontFamily::BOLD);
String resultText = String(clearedCount) + " items removed";
if (failedCount > 0) {
resultText += ", " + String(failedCount) + " failed";
}
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, resultText.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 10, resultText.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -95,8 +100,8 @@ void ClearCacheActivity::render() {
}
if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Failed to clear cache", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Check serial output for details");
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 20, "Failed to clear cache", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 10, "Check serial output for details");
const auto labels = mappedInput.mapLabels("« Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);

View File

@ -126,20 +126,28 @@ void OtaUpdateActivity::render() {
}
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int centerY = (pageHeight - bezelTop - bezelBottom) / 2 + bezelTop;
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Update", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Update", true, EpdFontFamily::BOLD);
if (state == CHECKING_FOR_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Checking for update...", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY, "Checking for update...", true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == WAITING_CONFIRMATION) {
renderer.drawCenteredText(UI_10_FONT_ID, 200, "New update available!", true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, 20, 250, "Current Version: " CROSSPOINT_VERSION);
renderer.drawText(UI_10_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str());
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 100, "New update available!", true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, centerY - 50, "Current Version: " CROSSPOINT_VERSION);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, centerY - 30, ("New Version: " + updater.getLatestVersion()).c_str());
const auto labels = mappedInput.mapLabels("Cancel", "Update", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
@ -148,33 +156,33 @@ void OtaUpdateActivity::render() {
}
if (state == UPDATE_IN_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 310, "Updating...", true, EpdFontFamily::BOLD);
renderer.drawRect(20, 350, pageWidth - 40, 50);
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
renderer.drawCenteredText(UI_10_FONT_ID, 420,
renderer.drawCenteredText(UI_10_FONT_ID, centerY - 40, "Updating...", true, EpdFontFamily::BOLD);
renderer.drawRect(20 + bezelLeft, centerY, pageWidth - 40 - bezelLeft - bezelRight, 50);
renderer.fillRect(24 + bezelLeft, centerY + 4, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44 - bezelLeft - bezelRight)), 42);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 70,
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
renderer.drawCenteredText(
UI_10_FONT_ID, 440,
UI_10_FONT_ID, centerY + 90,
(std::to_string(updater.processedSize) + " / " + std::to_string(updater.totalSize)).c_str());
renderer.displayBuffer();
return;
}
if (state == NO_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "No update available", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY, "No update available", true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update failed", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY, "Update failed", true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == FINISHED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update complete", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 350, "Press and hold power button to turn back on");
renderer.drawCenteredText(UI_10_FONT_ID, centerY, "Update complete", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, centerY + 50, "Press and hold power button to turn back on");
renderer.displayBuffer();
state = SHUTTING_DOWN;
return;

View File

@ -12,7 +12,10 @@
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
namespace {
constexpr int displaySettingsCount = 7;
// Visibility condition for bezel edge setting (only show when compensation > 0)
bool isBezelCompensationEnabled() { return SETTINGS.bezelCompensation > 0; }
constexpr int displaySettingsCount = 9;
const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
@ -24,7 +27,10 @@ const SettingInfo displaySettings[displaySettingsCount] = {
{"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}),
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::Value("Bezel Compensation", &CrossPointSettings::bezelCompensation, {0, 10, 1}),
SettingInfo::Enum("Bezel Edge", &CrossPointSettings::bezelCompensationEdge,
{"Bottom", "Top", "Left", "Right"}, isBezelCompensationEnabled)};
// Helper to get custom font names as a vector
std::vector<std::string> getCustomFontNamesVector() {
@ -213,23 +219,29 @@ void SettingsActivity::render() const {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Bezel compensation offsets
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelLeft = renderer.getBezelOffsetLeft();
const int bezelRight = renderer.getBezelOffsetRight();
const int bezelBottom = renderer.getBezelOffsetBottom();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, 15 + bezelTop, "Settings", true, EpdFontFamily::BOLD);
// Draw selection
renderer.fillRect(0, 60 + selectedCategoryIndex * 30 - 2, pageWidth - 1, 30);
renderer.fillRect(bezelLeft, 60 + bezelTop + selectedCategoryIndex * 30 - 2, pageWidth - 1 - bezelLeft - bezelRight, 30);
// Draw all categories
for (int i = 0; i < categoryCount; i++) {
const int categoryY = 60 + i * 30; // 30 pixels between categories
const int categoryY = 60 + bezelTop + i * 30; // 30 pixels between categories
// Draw category name
renderer.drawText(UI_10_FONT_ID, 20, categoryY, categoryNames[i], i != selectedCategoryIndex);
renderer.drawText(UI_10_FONT_ID, 20 + bezelLeft, categoryY, categoryNames[i], i != selectedCategoryIndex);
}
// Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - bezelRight - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60 - bezelBottom, CROSSPOINT_VERSION);
// Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");

View File

@ -8,7 +8,9 @@ void FullScreenMessageActivity::onEnter() {
Activity::onEnter();
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (renderer.getScreenHeight() - height) / 2;
const int bezelTop = renderer.getBezelOffsetTop();
const int bezelBottom = renderer.getBezelOffsetBottom();
const auto top = (renderer.getScreenHeight() - bezelTop - bezelBottom - height) / 2 + bezelTop;
renderer.clearScreen();
renderer.drawCenteredText(UI_10_FONT_ID, top, text.c_str(), true, style);

View File

@ -475,6 +475,8 @@ void setup() {
SETTINGS.loadFromFile();
// Apply high contrast mode from settings
setHighContrastMode(SETTINGS.displayContrast == 1);
// Apply bezel compensation from settings
renderer.setBezelCompensation(SETTINGS.bezelCompensation, SETTINGS.bezelCompensationEdge);
if (isWakeupByPowerButton()) {
// For normal wakeups, verify power button press duration