2025-12-08 22:06:09 +11:00
|
|
|
#include "GfxRenderer.h"
|
|
|
|
|
|
|
|
|
|
#include <Utf8.h>
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const {
|
|
|
|
|
switch (orientation) {
|
|
|
|
|
case Portrait: {
|
|
|
|
|
// Logical portrait (480x800) → panel (800x480)
|
|
|
|
|
// Rotation: 90 degrees clockwise
|
|
|
|
|
*rotatedX = y;
|
|
|
|
|
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case LandscapeClockwise: {
|
|
|
|
|
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
|
|
|
|
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x;
|
|
|
|
|
*rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case PortraitInverted: {
|
|
|
|
|
// Logical portrait (480x800) → panel (800x480)
|
|
|
|
|
// Rotation: 90 degrees counter-clockwise
|
|
|
|
|
*rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y;
|
|
|
|
|
*rotatedY = x;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case LandscapeCounterClockwise: {
|
|
|
|
|
// Logical landscape (800x480) aligned with panel orientation
|
|
|
|
|
*rotatedX = x;
|
|
|
|
|
*rotatedY = y;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|
|
|
|
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
|
|
|
|
|
|
|
|
|
// Early return if no framebuffer is set
|
|
|
|
|
if (!frameBuffer) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis());
|
2025-12-08 22:06:09 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
int rotatedX = 0;
|
|
|
|
|
int rotatedY = 0;
|
|
|
|
|
rotateCoordinates(x, y, &rotatedX, &rotatedY);
|
2025-12-08 22:06:09 +11:00
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
// Bounds checking against physical panel dimensions
|
2025-12-08 22:06:09 +11:00
|
|
|
if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 ||
|
|
|
|
|
rotatedY >= EInkDisplay::DISPLAY_HEIGHT) {
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY);
|
2025-12-08 22:06:09 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate byte position and bit position
|
|
|
|
|
const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
|
|
|
|
|
const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first
|
|
|
|
|
|
|
|
|
|
if (state) {
|
|
|
|
|
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
|
|
|
|
} else {
|
|
|
|
|
frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 12:11:36 +10:00
|
|
|
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
2025-12-08 22:06:09 +11:00
|
|
|
if (fontMap.count(fontId) == 0) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
2025-12-08 22:06:09 +11:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int w = 0, h = 0;
|
|
|
|
|
fontMap.at(fontId).getTextDimensions(text, &w, &h, style);
|
|
|
|
|
return w;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:52:19 +11:00
|
|
|
void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black,
|
2025-12-31 12:11:36 +10:00
|
|
|
const EpdFontFamily::Style style) const {
|
2025-12-08 22:52:19 +11:00
|
|
|
const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2;
|
|
|
|
|
drawText(fontId, x, y, text, black, style);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
|
2025-12-31 12:11:36 +10:00
|
|
|
const EpdFontFamily::Style style) const {
|
2025-12-28 21:30:01 +10:00
|
|
|
const int yPos = y + getFontAscenderSize(fontId);
|
2025-12-08 22:06:09 +11:00
|
|
|
int xpos = x;
|
|
|
|
|
|
|
|
|
|
// cannot draw a NULL / empty string
|
|
|
|
|
if (text == nullptr || *text == '\0') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fontMap.count(fontId) == 0) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
2025-12-08 22:06:09 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const auto font = fontMap.at(fontId);
|
|
|
|
|
|
|
|
|
|
// no printable characters
|
|
|
|
|
if (!font.hasPrintableChars(text, style)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t cp;
|
|
|
|
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
|
|
|
|
renderChar(font, cp, &xpos, &yPos, black, style);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
|
|
|
|
|
if (x1 == x2) {
|
|
|
|
|
if (y2 < y1) {
|
|
|
|
|
std::swap(y1, y2);
|
|
|
|
|
}
|
|
|
|
|
for (int y = y1; y <= y2; y++) {
|
|
|
|
|
drawPixel(x1, y, state);
|
|
|
|
|
}
|
|
|
|
|
} else if (y1 == y2) {
|
|
|
|
|
if (x2 < x1) {
|
|
|
|
|
std::swap(x1, x2);
|
|
|
|
|
}
|
|
|
|
|
for (int x = x1; x <= x2; x++) {
|
|
|
|
|
drawPixel(x, y1, state);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// TODO: Implement
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [GFX] Line drawing not supported\n", millis());
|
2025-12-08 22:06:09 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const {
|
|
|
|
|
drawLine(x, y, x + width - 1, y, state);
|
|
|
|
|
drawLine(x + width - 1, y, x + width - 1, y + height - 1, state);
|
|
|
|
|
drawLine(x + width - 1, y + height - 1, x, y + height - 1, state);
|
|
|
|
|
drawLine(x, y, x, y + height - 1, state);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
|
|
|
|
for (int fillY = y; fillY < y + height; fillY++) {
|
|
|
|
|
drawLine(x, fillY, x + width - 1, fillY, state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
int rotatedX = 0;
|
|
|
|
|
int rotatedY = 0;
|
|
|
|
|
rotateCoordinates(x, y, &rotatedX, &rotatedY);
|
fix: rotate origin in drawImage (#557)
## Summary
This was originally a comment in #499, but I'm making it its own PR,
because it doesn't depend on anything there and then I can base that PR
on this one.
Currently, `drawBitmap` is used for covers and sleep wallpaper, and
`drawImage` is used for the boot logo. `drawBitmap` goes row by row and
pixel by pixel, so it respects the renderer orientation. `drawImage`
just calls the `EInkDisplay`'s `drawImage`, which works in the eink
panel's native display orientation.
`drawImage` rotates the x,y coordinates where it's going to draw the
image, but doesn't account for the fact that the northwest corner in
portrait orientation becomes, the southwest corner of the image
rectangle in the native orientation. The boot and sleep activities
currently work around this by calculating the north*east* corner of
where the image should go, which becomes the northwest corner after
`rotateCoordinates`.
I think this wasn't really apparent because the CrossPoint logo is
rotationally symmetrical. The `EInkDisplay` `drawImage` always draws the
image in native orientation, but that looks the same for the "X" image.
If we rotate the origin coordinate in `GfxRenderer`'s `drawImage`, we
can use a much clearer northwest corner coordinate in the boot and sleep
activities. (And then, in #499, we can actually rotate the boot screen
to the user's preferred orientation).
This does *not* yet rotate the actual bits in the image; it's still
displayed in native orientation. This doesn't affect the
rotationally-symmetric logo, but if it's ever changed, we will probably
want to allocate a new `u8int[]` and transpose rows and columns if
necessary.
## Additional Context
I've created an additional branch on top of this to demonstrate by
replacing the logo with a non-rotationally-symmetrical image:
<img width="128" height="128" alt="Cat-in-a-pan-128-bw"
src="https://github.com/user-attachments/assets/d0b239bc-fe75-4ec8-bc02-9cf9436ca65f"
/>
https://github.com/crosspoint-reader/crosspoint-reader/compare/master...maeveynot:rotated-cat
(many thanks to https://notisrac.github.io/FileToCArray/)
As you can see, it is always drawn in native orientation, which makes it
sideways (turned clockwise) in portrait.
---
### AI Usage
No
Co-authored-by: Maeve Andrews <maeve@git.mail.maeveandrews.com>
2026-01-27 05:59:41 -06:00
|
|
|
// Rotate origin corner
|
|
|
|
|
switch (orientation) {
|
|
|
|
|
case Portrait:
|
|
|
|
|
rotatedY = rotatedY - height;
|
|
|
|
|
break;
|
|
|
|
|
case PortraitInverted:
|
|
|
|
|
rotatedX = rotatedX - width;
|
|
|
|
|
break;
|
|
|
|
|
case LandscapeClockwise:
|
|
|
|
|
rotatedY = rotatedY - height;
|
|
|
|
|
rotatedX = rotatedX - width;
|
|
|
|
|
break;
|
|
|
|
|
case LandscapeCounterClockwise:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// TODO: Rotate bits
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
2025-12-08 22:06:09 +11:00
|
|
|
}
|
|
|
|
|
|
2026-01-05 11:07:27 +01:00
|
|
|
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
|
|
|
|
|
const float cropX, const float cropY) const {
|
Add cover image display in *Continue Reading* card with framebuffer caching (#200)
## Summary
* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module,
Display the book cover image in the **"Continue Reading"** card on the
home screen, with fast navigation using framebuffer caching.
* **What changes are included?**
- Display book cover image in the "Continue Reading" card on home screen
- Load cover from cached BMP (same as sleep screen cover)
- Add framebuffer store/restore functions (`copyStoredBwBuffer`,
`freeStoredBwBuffer`) for fast navigation after initial render
- Fix `drawBitmap` scaling bug: apply scale to offset only, not to base
coordinates
- Add white text boxes behind title/author/continue reading label for
readability on cover
- Support both EPUB and XTC file cover images
- Increase HomeActivity task stack size from 2048 to 4096 for cover
image rendering
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).
- Performance: First render loads cover from SD card (~800ms),
subsequent navigation uses cached framebuffer (~instant)
- Memory: Framebuffer cache uses ~48KB (6 chunks × 8KB) while on home
screen, freed on exit
- Fallback: If cover image is not available, falls back to standard
text-only display
- The `drawBitmap` fix corrects a bug where screenY = (y + offset) scale
was incorrectly scaling the base coordinates. Now correctly uses screenY
= y + (offset scale)
2026-01-14 19:24:02 +09:00
|
|
|
// For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit)
|
|
|
|
|
if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) {
|
|
|
|
|
drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 08:45:14 +11:00
|
|
|
float scale = 1.0f;
|
|
|
|
|
bool isScaled = false;
|
2026-01-05 11:07:27 +01:00
|
|
|
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
|
|
|
|
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
|
|
|
|
Serial.printf("[%lu] [GFX] Cropping %dx%d by %dx%d pix, is %s\n", millis(), bitmap.getWidth(), bitmap.getHeight(),
|
|
|
|
|
cropPixX, cropPixY, bitmap.isTopDown() ? "top-down" : "bottom-up");
|
|
|
|
|
|
|
|
|
|
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
|
|
|
|
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
2025-12-19 08:45:14 +11:00
|
|
|
isScaled = true;
|
|
|
|
|
}
|
2026-01-05 11:07:27 +01:00
|
|
|
if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) {
|
|
|
|
|
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
2025-12-19 08:45:14 +11:00
|
|
|
isScaled = true;
|
|
|
|
|
}
|
2026-01-05 11:07:27 +01:00
|
|
|
Serial.printf("[%lu] [GFX] Scaling by %f - %s\n", millis(), scale, isScaled ? "scaled" : "not scaled");
|
2025-12-19 08:45:14 +11:00
|
|
|
|
2025-12-28 08:38:14 +09:00
|
|
|
// Calculate output row size (2 bits per pixel, packed into bytes)
|
|
|
|
|
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
|
|
|
|
const int outputRowSize = (bitmap.getWidth() + 3) / 4;
|
2025-12-19 08:45:14 +11:00
|
|
|
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
|
|
|
|
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
|
|
|
|
|
2025-12-21 03:36:59 +01:00
|
|
|
if (!outputRow || !rowBytes) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! Failed to allocate BMP row buffers\n", millis());
|
|
|
|
|
free(outputRow);
|
|
|
|
|
free(rowBytes);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 11:07:27 +01:00
|
|
|
for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) {
|
2025-12-19 08:45:14 +11:00
|
|
|
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
|
|
|
|
|
// Screen's (0, 0) is the top-left corner.
|
2026-01-05 11:07:27 +01:00
|
|
|
int screenY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
2025-12-19 08:45:14 +11:00
|
|
|
if (isScaled) {
|
|
|
|
|
screenY = std::floor(screenY * scale);
|
|
|
|
|
}
|
2026-01-05 11:07:27 +01:00
|
|
|
screenY += y; // the offset should not be scaled
|
2025-12-19 08:45:14 +11:00
|
|
|
if (screenY >= getScreenHeight()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 11:07:27 +01:00
|
|
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
2025-12-19 08:45:14 +11:00
|
|
|
Serial.printf("[%lu] [GFX] Failed to read row %d from bitmap\n", millis(), bmpY);
|
|
|
|
|
free(outputRow);
|
|
|
|
|
free(rowBytes);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-21 13:27:41 +01:00
|
|
|
if (screenY < 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 11:07:27 +01:00
|
|
|
if (bmpY < cropPixY) {
|
|
|
|
|
// Skip the row if it's outside the crop area
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
|
|
|
|
int screenX = bmpX - cropPixX;
|
2025-12-19 08:45:14 +11:00
|
|
|
if (isScaled) {
|
|
|
|
|
screenX = std::floor(screenX * scale);
|
|
|
|
|
}
|
2026-01-05 11:07:27 +01:00
|
|
|
screenX += x; // the offset should not be scaled
|
2025-12-19 08:45:14 +11:00
|
|
|
if (screenX >= getScreenWidth()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
Add cover image display in *Continue Reading* card with framebuffer caching (#200)
## Summary
* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module,
Display the book cover image in the **"Continue Reading"** card on the
home screen, with fast navigation using framebuffer caching.
* **What changes are included?**
- Display book cover image in the "Continue Reading" card on home screen
- Load cover from cached BMP (same as sleep screen cover)
- Add framebuffer store/restore functions (`copyStoredBwBuffer`,
`freeStoredBwBuffer`) for fast navigation after initial render
- Fix `drawBitmap` scaling bug: apply scale to offset only, not to base
coordinates
- Add white text boxes behind title/author/continue reading label for
readability on cover
- Support both EPUB and XTC file cover images
- Increase HomeActivity task stack size from 2048 to 4096 for cover
image rendering
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).
- Performance: First render loads cover from SD card (~800ms),
subsequent navigation uses cached framebuffer (~instant)
- Memory: Framebuffer cache uses ~48KB (6 chunks × 8KB) while on home
screen, freed on exit
- Fallback: If cover image is not available, falls back to standard
text-only display
- The `drawBitmap` fix corrects a bug where screenY = (y + offset) scale
was incorrectly scaling the base coordinates. Now correctly uses screenY
= y + (offset scale)
2026-01-14 19:24:02 +09:00
|
|
|
if (screenX < 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-12-19 08:45:14 +11:00
|
|
|
|
|
|
|
|
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
|
|
|
|
|
|
|
|
|
if (renderMode == BW && val < 3) {
|
|
|
|
|
drawPixel(screenX, screenY);
|
|
|
|
|
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
|
|
|
|
drawPixel(screenX, screenY, false);
|
|
|
|
|
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
|
|
|
|
drawPixel(screenX, screenY, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
free(outputRow);
|
|
|
|
|
free(rowBytes);
|
|
|
|
|
}
|
|
|
|
|
|
Add cover image display in *Continue Reading* card with framebuffer caching (#200)
## Summary
* **What is the goal of this PR?** (e.g., Fixes a bug in the user
authentication module,
Display the book cover image in the **"Continue Reading"** card on the
home screen, with fast navigation using framebuffer caching.
* **What changes are included?**
- Display book cover image in the "Continue Reading" card on home screen
- Load cover from cached BMP (same as sleep screen cover)
- Add framebuffer store/restore functions (`copyStoredBwBuffer`,
`freeStoredBwBuffer`) for fast navigation after initial render
- Fix `drawBitmap` scaling bug: apply scale to offset only, not to base
coordinates
- Add white text boxes behind title/author/continue reading label for
readability on cover
- Support both EPUB and XTC file cover images
- Increase HomeActivity task stack size from 2048 to 4096 for cover
image rendering
## Additional Context
* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks, specific areas to
focus on).
- Performance: First render loads cover from SD card (~800ms),
subsequent navigation uses cached framebuffer (~instant)
- Memory: Framebuffer cache uses ~48KB (6 chunks × 8KB) while on home
screen, freed on exit
- Fallback: If cover image is not available, falls back to standard
text-only display
- The `drawBitmap` fix corrects a bug where screenY = (y + offset) scale
was incorrectly scaling the base coordinates. Now correctly uses screenY
= y + (offset scale)
2026-01-14 19:24:02 +09:00
|
|
|
void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
|
|
|
|
|
const int maxHeight) const {
|
|
|
|
|
float scale = 1.0f;
|
|
|
|
|
bool isScaled = false;
|
|
|
|
|
if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
|
|
|
|
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
|
|
|
|
isScaled = true;
|
|
|
|
|
}
|
|
|
|
|
if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
|
|
|
|
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
|
|
|
|
|
isScaled = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For 1-bit BMP, output is still 2-bit packed (for consistency with readNextRow)
|
|
|
|
|
const int outputRowSize = (bitmap.getWidth() + 3) / 4;
|
|
|
|
|
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
|
|
|
|
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
|
|
|
|
|
|
|
|
|
if (!outputRow || !rowBytes) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! Failed to allocate 1-bit BMP row buffers\n", millis());
|
|
|
|
|
free(outputRow);
|
|
|
|
|
free(rowBytes);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
|
|
|
|
// Read rows sequentially using readNextRow
|
|
|
|
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] Failed to read row %d from 1-bit bitmap\n", millis(), bmpY);
|
|
|
|
|
free(outputRow);
|
|
|
|
|
free(rowBytes);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate screen Y based on whether BMP is top-down or bottom-up
|
|
|
|
|
const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
|
|
|
|
int screenY = y + (isScaled ? static_cast<int>(std::floor(bmpYOffset * scale)) : bmpYOffset);
|
|
|
|
|
if (screenY >= getScreenHeight()) {
|
|
|
|
|
continue; // Continue reading to keep row counter in sync
|
|
|
|
|
}
|
|
|
|
|
if (screenY < 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
|
|
|
|
|
int screenX = x + (isScaled ? static_cast<int>(std::floor(bmpX * scale)) : bmpX);
|
|
|
|
|
if (screenX >= getScreenWidth()) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (screenX < 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get 2-bit value (result of readNextRow quantization)
|
|
|
|
|
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
|
|
|
|
|
|
|
|
|
// For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3)
|
|
|
|
|
// val < 3 means black pixel (draw it)
|
|
|
|
|
if (val < 3) {
|
|
|
|
|
drawPixel(screenX, screenY, true);
|
|
|
|
|
}
|
|
|
|
|
// White pixels (val == 3) are not drawn (leave background)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
free(outputRow);
|
|
|
|
|
free(rowBytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state) const {
|
|
|
|
|
if (numPoints < 3) return;
|
|
|
|
|
|
|
|
|
|
// Find bounding box
|
|
|
|
|
int minY = yPoints[0], maxY = yPoints[0];
|
|
|
|
|
for (int i = 1; i < numPoints; i++) {
|
|
|
|
|
if (yPoints[i] < minY) minY = yPoints[i];
|
|
|
|
|
if (yPoints[i] > maxY) maxY = yPoints[i];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clip to screen
|
|
|
|
|
if (minY < 0) minY = 0;
|
|
|
|
|
if (maxY >= getScreenHeight()) maxY = getScreenHeight() - 1;
|
|
|
|
|
|
|
|
|
|
// Allocate node buffer for scanline algorithm
|
|
|
|
|
auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int)));
|
|
|
|
|
if (!nodeX) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! Failed to allocate polygon node buffer\n", millis());
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scanline fill algorithm
|
|
|
|
|
for (int scanY = minY; scanY <= maxY; scanY++) {
|
|
|
|
|
int nodes = 0;
|
|
|
|
|
|
|
|
|
|
// Find all intersection points with edges
|
|
|
|
|
int j = numPoints - 1;
|
|
|
|
|
for (int i = 0; i < numPoints; i++) {
|
|
|
|
|
if ((yPoints[i] < scanY && yPoints[j] >= scanY) || (yPoints[j] < scanY && yPoints[i] >= scanY)) {
|
|
|
|
|
// Calculate X intersection using fixed-point to avoid float
|
|
|
|
|
int dy = yPoints[j] - yPoints[i];
|
|
|
|
|
if (dy != 0) {
|
|
|
|
|
nodeX[nodes++] = xPoints[i] + (scanY - yPoints[i]) * (xPoints[j] - xPoints[i]) / dy;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
j = i;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort nodes by X (simple bubble sort, numPoints is small)
|
|
|
|
|
for (int i = 0; i < nodes - 1; i++) {
|
|
|
|
|
for (int k = i + 1; k < nodes; k++) {
|
|
|
|
|
if (nodeX[i] > nodeX[k]) {
|
|
|
|
|
int temp = nodeX[i];
|
|
|
|
|
nodeX[i] = nodeX[k];
|
|
|
|
|
nodeX[k] = temp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fill between pairs of nodes
|
|
|
|
|
for (int i = 0; i < nodes - 1; i += 2) {
|
|
|
|
|
int startX = nodeX[i];
|
|
|
|
|
int endX = nodeX[i + 1];
|
|
|
|
|
|
|
|
|
|
// Clip to screen
|
|
|
|
|
if (startX < 0) startX = 0;
|
|
|
|
|
if (endX >= getScreenWidth()) endX = getScreenWidth() - 1;
|
|
|
|
|
|
|
|
|
|
// Draw horizontal line
|
|
|
|
|
for (int x = startX; x <= endX; x++) {
|
|
|
|
|
drawPixel(x, scanY, state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
free(nodeX);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); }
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::invertScreen() const {
|
|
|
|
|
uint8_t* buffer = einkDisplay.getFrameBuffer();
|
2025-12-21 03:34:58 +01:00
|
|
|
if (!buffer) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis());
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-08 22:06:09 +11:00
|
|
|
for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) {
|
|
|
|
|
buffer[i] = ~buffer[i];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const {
|
|
|
|
|
einkDisplay.displayBuffer(refreshMode);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 23:10:41 +01:00
|
|
|
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
2025-12-31 12:11:36 +10:00
|
|
|
const EpdFontFamily::Style style) const {
|
2025-12-30 23:10:41 +01:00
|
|
|
std::string item = text;
|
|
|
|
|
int itemWidth = getTextWidth(fontId, item.c_str(), style);
|
|
|
|
|
while (itemWidth > maxWidth && item.length() > 8) {
|
|
|
|
|
item.replace(item.length() - 5, 5, "...");
|
|
|
|
|
itemWidth = getTextWidth(fontId, item.c_str(), style);
|
|
|
|
|
}
|
|
|
|
|
return item;
|
|
|
|
|
}
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
|
|
|
|
int GfxRenderer::getScreenWidth() const {
|
|
|
|
|
switch (orientation) {
|
|
|
|
|
case Portrait:
|
|
|
|
|
case PortraitInverted:
|
|
|
|
|
// 480px wide in portrait logical coordinates
|
|
|
|
|
return EInkDisplay::DISPLAY_HEIGHT;
|
|
|
|
|
case LandscapeClockwise:
|
|
|
|
|
case LandscapeCounterClockwise:
|
|
|
|
|
// 800px wide in landscape logical coordinates
|
|
|
|
|
return EInkDisplay::DISPLAY_WIDTH;
|
|
|
|
|
}
|
|
|
|
|
return EInkDisplay::DISPLAY_HEIGHT;
|
2025-12-17 00:17:49 +11:00
|
|
|
}
|
2025-12-08 22:06:09 +11:00
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
int GfxRenderer::getScreenHeight() const {
|
|
|
|
|
switch (orientation) {
|
|
|
|
|
case Portrait:
|
|
|
|
|
case PortraitInverted:
|
|
|
|
|
// 800px tall in portrait logical coordinates
|
|
|
|
|
return EInkDisplay::DISPLAY_WIDTH;
|
|
|
|
|
case LandscapeClockwise:
|
|
|
|
|
case LandscapeCounterClockwise:
|
|
|
|
|
// 480px tall in landscape logical coordinates
|
|
|
|
|
return EInkDisplay::DISPLAY_HEIGHT;
|
|
|
|
|
}
|
|
|
|
|
return EInkDisplay::DISPLAY_WIDTH;
|
|
|
|
|
}
|
2025-12-08 22:06:09 +11:00
|
|
|
|
|
|
|
|
int GfxRenderer::getSpaceWidth(const int fontId) const {
|
|
|
|
|
if (fontMap.count(fontId) == 0) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
2025-12-08 22:06:09 +11:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 12:11:36 +10:00
|
|
|
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
2025-12-08 22:06:09 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-28 21:30:01 +10:00
|
|
|
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
|
|
|
|
if (fontMap.count(fontId) == 0) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 12:11:36 +10:00
|
|
|
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
2025-12-28 21:30:01 +10:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
int GfxRenderer::getLineHeight(const int fontId) const {
|
|
|
|
|
if (fontMap.count(fontId) == 0) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
2025-12-08 22:06:09 +11:00
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-31 12:11:36 +10:00
|
|
|
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->advanceY;
|
2025-12-08 22:06:09 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-25 19:54:02 -05:00
|
|
|
void GfxRenderer::drawButtonHints(const int fontId, const char* btn1, const char* btn2, const char* btn3,
|
2026-01-15 06:23:36 -06:00
|
|
|
const char* btn4) {
|
|
|
|
|
const Orientation orig_orientation = getOrientation();
|
|
|
|
|
setOrientation(Orientation::Portrait);
|
|
|
|
|
|
2025-12-25 19:54:02 -05:00
|
|
|
const int pageHeight = getScreenHeight();
|
|
|
|
|
constexpr int buttonWidth = 106;
|
|
|
|
|
constexpr int buttonHeight = 40;
|
|
|
|
|
constexpr int buttonY = 40; // Distance from bottom
|
2025-12-28 21:30:01 +10:00
|
|
|
constexpr int textYOffset = 7; // Distance from top of button to text baseline
|
2025-12-25 19:54:02 -05:00
|
|
|
constexpr int buttonPositions[] = {25, 130, 245, 350};
|
|
|
|
|
const char* labels[] = {btn1, btn2, btn3, btn4};
|
|
|
|
|
|
|
|
|
|
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];
|
2026-01-15 06:23:36 -06:00
|
|
|
fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false);
|
2025-12-25 19:54:02 -05:00
|
|
|
drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight);
|
|
|
|
|
const int textWidth = getTextWidth(fontId, labels[i]);
|
|
|
|
|
const int textX = x + (buttonWidth - 1 - textWidth) / 2;
|
|
|
|
|
drawText(fontId, textX, pageHeight - buttonY + textYOffset, labels[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-15 06:23:36 -06:00
|
|
|
|
|
|
|
|
setOrientation(orig_orientation);
|
2025-12-25 19:54:02 -05:00
|
|
|
}
|
|
|
|
|
|
2026-01-03 03:17:53 -05:00
|
|
|
void GfxRenderer::drawSideButtonHints(const int fontId, const char* topBtn, const char* bottomBtn) const {
|
|
|
|
|
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
|
|
|
|
|
// Position for the button group - buttons share a border so they're adjacent
|
|
|
|
|
constexpr int topButtonY = 345; // Top button position
|
|
|
|
|
|
|
|
|
|
const char* labels[] = {topBtn, bottomBtn};
|
|
|
|
|
|
|
|
|
|
// Draw the shared border for both buttons as one unit
|
|
|
|
|
const int x = screenWidth - buttonX - buttonWidth;
|
|
|
|
|
|
|
|
|
|
// Draw top button outline (3 sides, bottom open)
|
|
|
|
|
if (topBtn != nullptr && topBtn[0] != '\0') {
|
|
|
|
|
drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY); // Top
|
|
|
|
|
drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1); // Left
|
|
|
|
|
drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1); // Right
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw shared middle border
|
|
|
|
|
if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) {
|
|
|
|
|
drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight); // Shared border
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw bottom button outline (3 sides, top is shared)
|
|
|
|
|
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
|
|
|
|
drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1); // Left
|
|
|
|
|
drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1,
|
|
|
|
|
topButtonY + 2 * buttonHeight - 1); // Right
|
|
|
|
|
drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1, topButtonY + 2 * buttonHeight - 1); // Bottom
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Draw text for each button
|
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
|
|
|
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
|
|
|
|
const int y = topButtonY + i * buttonHeight;
|
|
|
|
|
|
|
|
|
|
// Draw rotated text centered in the button
|
|
|
|
|
const int textWidth = getTextWidth(fontId, labels[i]);
|
|
|
|
|
const int textHeight = getTextHeight(fontId);
|
|
|
|
|
|
|
|
|
|
// Center the rotated text in the button
|
|
|
|
|
const int textX = x + (buttonWidth - textHeight) / 2;
|
|
|
|
|
const int textY = y + (buttonHeight + textWidth) / 2;
|
|
|
|
|
|
|
|
|
|
drawTextRotated90CW(fontId, textX, textY, labels[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int GfxRenderer::getTextHeight(const int fontId) const {
|
|
|
|
|
if (fontMap.count(fontId) == 0) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
return fontMap.at(fontId).getData(EpdFontFamily::REGULAR)->ascender;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
|
|
|
|
|
const EpdFontFamily::Style style) const {
|
|
|
|
|
// Cannot draw a NULL / empty string
|
|
|
|
|
if (text == nullptr || *text == '\0') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fontMap.count(fontId) == 0) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const auto font = fontMap.at(fontId);
|
|
|
|
|
|
|
|
|
|
// No printable characters
|
|
|
|
|
if (!font.hasPrintableChars(text, style)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For 90° clockwise rotation:
|
|
|
|
|
// Original (glyphX, glyphY) -> Rotated (glyphY, -glyphX)
|
|
|
|
|
// Text reads from bottom to top
|
|
|
|
|
|
|
|
|
|
int yPos = y; // Current Y position (decreases as we draw characters)
|
|
|
|
|
|
|
|
|
|
uint32_t cp;
|
|
|
|
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
|
|
|
|
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
|
|
|
|
if (!glyph) {
|
2026-01-19 05:58:43 -06:00
|
|
|
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
2026-01-03 03:17:53 -05:00
|
|
|
}
|
|
|
|
|
if (!glyph) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int is2Bit = font.getData(style)->is2Bit;
|
|
|
|
|
const uint32_t offset = glyph->dataOffset;
|
|
|
|
|
const uint8_t width = glyph->width;
|
|
|
|
|
const uint8_t height = glyph->height;
|
|
|
|
|
const int left = glyph->left;
|
|
|
|
|
const int top = glyph->top;
|
|
|
|
|
|
|
|
|
|
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
|
|
|
|
|
|
|
|
|
|
if (bitmap != nullptr) {
|
|
|
|
|
for (int glyphY = 0; glyphY < height; glyphY++) {
|
|
|
|
|
for (int glyphX = 0; glyphX < width; glyphX++) {
|
|
|
|
|
const int pixelPosition = glyphY * width + glyphX;
|
|
|
|
|
|
|
|
|
|
// 90° clockwise rotation transformation:
|
|
|
|
|
// screenX = x + (ascender - top + glyphY)
|
|
|
|
|
// screenY = yPos - (left + glyphX)
|
|
|
|
|
const int screenX = x + (font.getData(style)->ascender - top + glyphY);
|
|
|
|
|
const int screenY = yPos - left - glyphX;
|
|
|
|
|
|
|
|
|
|
if (is2Bit) {
|
|
|
|
|
const uint8_t byte = bitmap[pixelPosition / 4];
|
|
|
|
|
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
|
|
|
|
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
|
|
|
|
|
|
|
|
|
|
if (renderMode == BW && bmpVal < 3) {
|
|
|
|
|
drawPixel(screenX, screenY, black);
|
|
|
|
|
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
|
|
|
|
drawPixel(screenX, screenY, false);
|
|
|
|
|
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
|
|
|
|
|
drawPixel(screenX, screenY, false);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const uint8_t byte = bitmap[pixelPosition / 8];
|
|
|
|
|
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
|
|
|
|
|
|
|
|
|
if ((byte >> bit_index) & 1) {
|
|
|
|
|
drawPixel(screenX, screenY, black);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Move to next character position (going up, so decrease Y)
|
|
|
|
|
yPos -= glyph->advanceX;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 19:36:01 +11:00
|
|
|
uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
|
2025-12-13 16:02:27 +11:00
|
|
|
|
2025-12-17 00:17:49 +11:00
|
|
|
size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; }
|
2025-12-08 22:06:09 +11:00
|
|
|
|
2025-12-13 16:02:27 +11:00
|
|
|
void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); }
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); }
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); }
|
|
|
|
|
|
|
|
|
|
void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); }
|
|
|
|
|
|
2025-12-17 01:39:22 +11:00
|
|
|
void GfxRenderer::freeBwBufferChunks() {
|
|
|
|
|
for (auto& bwBufferChunk : bwBufferChunks) {
|
|
|
|
|
if (bwBufferChunk) {
|
|
|
|
|
free(bwBufferChunk);
|
|
|
|
|
bwBufferChunk = nullptr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 00:17:49 +11:00
|
|
|
/**
|
|
|
|
|
* This should be called before grayscale buffers are populated.
|
|
|
|
|
* A `restoreBwBuffer` call should always follow the grayscale render if this method was called.
|
2025-12-17 01:39:22 +11:00
|
|
|
* Uses chunked allocation to avoid needing 48KB of contiguous memory.
|
2025-12-28 23:56:05 +09:00
|
|
|
* Returns true if buffer was stored successfully, false if allocation failed.
|
2025-12-17 00:17:49 +11:00
|
|
|
*/
|
2025-12-28 23:56:05 +09:00
|
|
|
bool GfxRenderer::storeBwBuffer() {
|
2025-12-17 01:39:22 +11:00
|
|
|
const uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
2025-12-21 03:34:58 +01:00
|
|
|
if (!frameBuffer) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis());
|
2025-12-28 23:56:05 +09:00
|
|
|
return false;
|
2025-12-21 03:34:58 +01:00
|
|
|
}
|
2025-12-17 01:39:22 +11:00
|
|
|
|
|
|
|
|
// Allocate and copy each chunk
|
|
|
|
|
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
|
|
|
|
// Check if any chunks are already allocated
|
|
|
|
|
if (bwBufferChunks[i]) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk\n",
|
|
|
|
|
millis(), i);
|
|
|
|
|
free(bwBufferChunks[i]);
|
|
|
|
|
bwBufferChunks[i] = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
|
|
|
|
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
|
|
|
|
|
|
|
|
|
if (!bwBufferChunks[i]) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! Failed to allocate BW buffer chunk %zu (%zu bytes)\n", millis(), i,
|
|
|
|
|
BW_BUFFER_CHUNK_SIZE);
|
|
|
|
|
// Free previously allocated chunks
|
|
|
|
|
freeBwBufferChunks();
|
2025-12-28 23:56:05 +09:00
|
|
|
return false;
|
2025-12-17 01:39:22 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
2025-12-17 00:17:49 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-17 01:39:22 +11:00
|
|
|
Serial.printf("[%lu] [GFX] Stored BW buffer in %zu chunks (%zu bytes each)\n", millis(), BW_BUFFER_NUM_CHUNKS,
|
|
|
|
|
BW_BUFFER_CHUNK_SIZE);
|
2025-12-28 23:56:05 +09:00
|
|
|
return true;
|
2025-12-17 00:17:49 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This can only be called if `storeBwBuffer` was called prior to the grayscale render.
|
|
|
|
|
* It should be called to restore the BW buffer state after grayscale rendering is complete.
|
2025-12-17 01:39:22 +11:00
|
|
|
* Uses chunked restoration to match chunked storage.
|
2025-12-17 00:17:49 +11:00
|
|
|
*/
|
|
|
|
|
void GfxRenderer::restoreBwBuffer() {
|
2025-12-17 01:39:22 +11:00
|
|
|
// Check if any all chunks are allocated
|
|
|
|
|
bool missingChunks = false;
|
|
|
|
|
for (const auto& bwBufferChunk : bwBufferChunks) {
|
|
|
|
|
if (!bwBufferChunk) {
|
|
|
|
|
missingChunks = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (missingChunks) {
|
|
|
|
|
freeBwBufferChunks();
|
2025-12-17 00:17:49 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 01:39:22 +11:00
|
|
|
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
2025-12-21 03:34:58 +01:00
|
|
|
if (!frameBuffer) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis());
|
|
|
|
|
freeBwBufferChunks();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-17 01:39:22 +11:00
|
|
|
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
|
|
|
|
// Check if chunk is missing
|
|
|
|
|
if (!bwBufferChunks[i]) {
|
|
|
|
|
Serial.printf("[%lu] [GFX] !! BW buffer chunks not stored - this is likely a bug\n", millis());
|
|
|
|
|
freeBwBufferChunks();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
|
|
|
|
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
|
|
|
|
|
|
|
|
|
|
freeBwBufferChunks();
|
|
|
|
|
Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis());
|
2025-12-17 00:17:49 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-28 23:56:05 +09:00
|
|
|
/**
|
|
|
|
|
* Cleanup grayscale buffers using the current frame buffer.
|
|
|
|
|
* Use this when BW buffer was re-rendered instead of stored/restored.
|
|
|
|
|
*/
|
|
|
|
|
void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
|
|
|
|
uint8_t* frameBuffer = einkDisplay.getFrameBuffer();
|
|
|
|
|
if (frameBuffer) {
|
|
|
|
|
einkDisplay.cleanupGrayscaleBuffers(frameBuffer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y,
|
2025-12-31 12:11:36 +10:00
|
|
|
const bool pixelState, const EpdFontFamily::Style style) const {
|
2025-12-08 22:06:09 +11:00
|
|
|
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
|
|
|
|
|
if (!glyph) {
|
2026-01-19 05:58:43 -06:00
|
|
|
glyph = fontFamily.getGlyph(REPLACEMENT_GLYPH, style);
|
2025-12-08 22:06:09 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// no glyph?
|
|
|
|
|
if (!glyph) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [GFX] No glyph for codepoint %d\n", millis(), cp);
|
2025-12-08 22:06:09 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int is2Bit = fontFamily.getData(style)->is2Bit;
|
|
|
|
|
const uint32_t offset = glyph->dataOffset;
|
|
|
|
|
const uint8_t width = glyph->width;
|
|
|
|
|
const uint8_t height = glyph->height;
|
|
|
|
|
const int left = glyph->left;
|
|
|
|
|
|
|
|
|
|
const uint8_t* bitmap = nullptr;
|
|
|
|
|
bitmap = &fontFamily.getData(style)->bitmap[offset];
|
|
|
|
|
|
|
|
|
|
if (bitmap != nullptr) {
|
|
|
|
|
for (int glyphY = 0; glyphY < height; glyphY++) {
|
|
|
|
|
const int screenY = *y - glyph->top + glyphY;
|
|
|
|
|
for (int glyphX = 0; glyphX < width; glyphX++) {
|
|
|
|
|
const int pixelPosition = glyphY * width + glyphX;
|
|
|
|
|
const int screenX = *x + left + glyphX;
|
|
|
|
|
|
|
|
|
|
if (is2Bit) {
|
|
|
|
|
const uint8_t byte = bitmap[pixelPosition / 4];
|
|
|
|
|
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
2025-12-16 02:16:35 +11:00
|
|
|
// the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black
|
|
|
|
|
// we swap this to better match the way images and screen think about colors:
|
|
|
|
|
// 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white
|
|
|
|
|
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
|
2025-12-08 22:06:09 +11:00
|
|
|
|
2025-12-16 02:16:35 +11:00
|
|
|
if (renderMode == BW && bmpVal < 3) {
|
|
|
|
|
// Black (also paints over the grays in BW mode)
|
2025-12-08 22:06:09 +11:00
|
|
|
drawPixel(screenX, screenY, pixelState);
|
2025-12-16 02:16:35 +11:00
|
|
|
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
|
|
|
|
// Light gray (also mark the MSB if it's going to be a dark gray too)
|
|
|
|
|
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
|
2025-12-08 22:06:09 +11:00
|
|
|
drawPixel(screenX, screenY, false);
|
2025-12-16 02:16:35 +11:00
|
|
|
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
|
|
|
|
|
// Dark gray
|
2025-12-08 22:06:09 +11:00
|
|
|
drawPixel(screenX, screenY, false);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const uint8_t byte = bitmap[pixelPosition / 8];
|
|
|
|
|
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
|
|
|
|
|
|
|
|
|
if ((byte >> bit_index) & 1) {
|
|
|
|
|
drawPixel(screenX, screenY, pixelState);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*x += glyph->advanceX;
|
|
|
|
|
}
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
|
|
|
|
|
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
|
|
|
|
|
switch (orientation) {
|
|
|
|
|
case Portrait:
|
|
|
|
|
*outTop = VIEWABLE_MARGIN_TOP;
|
|
|
|
|
*outRight = VIEWABLE_MARGIN_RIGHT;
|
|
|
|
|
*outBottom = VIEWABLE_MARGIN_BOTTOM;
|
|
|
|
|
*outLeft = VIEWABLE_MARGIN_LEFT;
|
|
|
|
|
break;
|
|
|
|
|
case LandscapeClockwise:
|
|
|
|
|
*outTop = VIEWABLE_MARGIN_LEFT;
|
|
|
|
|
*outRight = VIEWABLE_MARGIN_TOP;
|
|
|
|
|
*outBottom = VIEWABLE_MARGIN_RIGHT;
|
|
|
|
|
*outLeft = VIEWABLE_MARGIN_BOTTOM;
|
|
|
|
|
break;
|
|
|
|
|
case PortraitInverted:
|
|
|
|
|
*outTop = VIEWABLE_MARGIN_BOTTOM;
|
|
|
|
|
*outRight = VIEWABLE_MARGIN_LEFT;
|
|
|
|
|
*outBottom = VIEWABLE_MARGIN_TOP;
|
|
|
|
|
*outLeft = VIEWABLE_MARGIN_RIGHT;
|
|
|
|
|
break;
|
|
|
|
|
case LandscapeCounterClockwise:
|
|
|
|
|
*outTop = VIEWABLE_MARGIN_RIGHT;
|
|
|
|
|
*outRight = VIEWABLE_MARGIN_BOTTOM;
|
|
|
|
|
*outBottom = VIEWABLE_MARGIN_LEFT;
|
|
|
|
|
*outLeft = VIEWABLE_MARGIN_TOP;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|