- rounded rects

- background fill
- border radius
- container paddings
- fix navigation in home
This commit is contained in:
Brackyt 2026-01-25 15:23:03 +01:00
parent d54f3c5143
commit 7a5c1e8e0e
9 changed files with 482 additions and 171 deletions

View File

@ -69,6 +69,30 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
}
}
bool GfxRenderer::readPixel(const int x, const int y) const {
uint8_t *frameBuffer = display.getFrameBuffer();
if (!frameBuffer) {
return false;
}
int rotatedX = 0;
int rotatedY = 0;
rotateCoordinates(x, y, &rotatedX, &rotatedY);
// Bounds checking against physical panel dimensions
if (rotatedX < 0 || rotatedX >= HalDisplay::DISPLAY_WIDTH ||
rotatedY < 0 || rotatedY >= HalDisplay::DISPLAY_HEIGHT) {
return false;
}
const uint16_t byteIndex =
rotatedY * HalDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8);
const uint8_t bitPosition = 7 - (rotatedX % 8);
// Bit cleared = black, bit set = white
return !(frameBuffer[byteIndex] & (1 << bitPosition));
}
int GfxRenderer::getTextWidth(const int fontId, const char *text,
const EpdFontFamily::Style style) const {
if (fontMap.count(fontId) == 0) {
@ -152,7 +176,7 @@ void GfxRenderer::drawRect(const int x, const int y, const int width,
void GfxRenderer::fillRect(const int x, const int y, const int width,
const int height, const bool state) const {
uint8_t *frameBuffer = einkDisplay.getFrameBuffer();
uint8_t *frameBuffer = display.getFrameBuffer();
if (!frameBuffer) {
return;
}
@ -179,9 +203,9 @@ void GfxRenderer::fillRect(const int x, const int y, const int width,
const uint8_t mask = 1 << physXBit;
for (int sx = x1; sx <= x2; sx++) {
const int physY = EInkDisplay::DISPLAY_HEIGHT - 1 - sx;
const int physY = HalDisplay::DISPLAY_HEIGHT - 1 - sx;
const uint16_t byteIndex =
physY * EInkDisplay::DISPLAY_WIDTH_BYTES + physXByte;
physY * HalDisplay::DISPLAY_WIDTH_BYTES + physXByte;
if (state) {
frameBuffer[byteIndex] &= ~mask; // Black
@ -196,7 +220,7 @@ void GfxRenderer::fillRect(const int x, const int y, const int width,
// Optimized path for PortraitInverted
if (orientation == PortraitInverted) {
for (int sy = y1; sy <= y2; sy++) {
const int physX = EInkDisplay::DISPLAY_WIDTH - 1 - sy;
const int physX = HalDisplay::DISPLAY_WIDTH - 1 - sy;
const uint8_t physXByte = physX / 8;
const uint8_t physXBit = 7 - (physX % 8);
const uint8_t mask = 1 << physXBit;
@ -204,7 +228,7 @@ void GfxRenderer::fillRect(const int x, const int y, const int width,
for (int sx = x1; sx <= x2; sx++) {
const int physY = sx;
const uint16_t byteIndex =
physY * EInkDisplay::DISPLAY_WIDTH_BYTES + physXByte;
physY * HalDisplay::DISPLAY_WIDTH_BYTES + physXByte;
if (state) {
frameBuffer[byteIndex] &= ~mask;
@ -220,7 +244,7 @@ void GfxRenderer::fillRect(const int x, const int y, const int width,
if (orientation == LandscapeCounterClockwise) {
for (int sy = y1; sy <= y2; sy++) {
const int physY = sy;
const uint16_t rowOffset = physY * EInkDisplay::DISPLAY_WIDTH_BYTES;
const uint16_t rowOffset = physY * HalDisplay::DISPLAY_WIDTH_BYTES;
// Fill full bytes where possible
const int physX1 = x1;
@ -258,6 +282,198 @@ void GfxRenderer::fillRect(const int x, const int y, const int width,
}
}
void GfxRenderer::fillRectDithered(const int x, const int y, const int width,
const int height, const uint8_t grayLevel) const {
// Simulate grayscale using dithering patterns
// 0x00 = black, 0xFF = white, values in between = dithered
if (grayLevel == 0x00) {
fillRect(x, y, width, height, true); // Solid black
return;
}
if (grayLevel >= 0xF0) {
fillRect(x, y, width, height, false); // Solid white
return;
}
// Use ordered dithering (Bayer matrix 2x2)
const int screenWidth = getScreenWidth();
const int screenHeight = getScreenHeight();
const int x1 = std::max(0, x);
const int y1 = std::max(0, y);
const int x2 = std::min(screenWidth - 1, x + width - 1);
const int y2 = std::min(screenHeight - 1, y + height - 1);
if (x1 > x2 || y1 > y2) return;
int threshold = (grayLevel * 4) / 255;
for (int sy = y1; sy <= y2; sy++) {
for (int sx = x1; sx <= x2; sx++) {
int bayerValue;
int px = sx % 2;
int py = sy % 2;
if (px == 0 && py == 0) bayerValue = 0;
else if (px == 1 && py == 0) bayerValue = 2;
else if (px == 0 && py == 1) bayerValue = 3;
else bayerValue = 1;
bool isBlack = bayerValue >= threshold;
drawPixel(sx, sy, isBlack);
}
}
}
void GfxRenderer::drawRoundedRect(const int x, const int y, const int width,
const int height, const int radius, const bool state) const {
if (radius <= 0) {
drawRect(x, y, width, height, state);
return;
}
int r = std::min(radius, std::min(width / 2, height / 2));
int cx, cy;
int px = 0, py = r;
int d = 1 - r;
while (px <= py) {
cx = x + r; cy = y + r;
drawPixel(cx - py, cy - px, state);
drawPixel(cx - px, cy - py, state);
cx = x + width - 1 - r; cy = y + r;
drawPixel(cx + py, cy - px, state);
drawPixel(cx + px, cy - py, state);
cx = x + r; cy = y + height - 1 - r;
drawPixel(cx - py, cy + px, state);
drawPixel(cx - px, cy + py, state);
cx = x + width - 1 - r; cy = y + height - 1 - r;
drawPixel(cx + py, cy + px, state);
drawPixel(cx + px, cy + py, state);
if (d < 0) {
d += 2 * px + 3;
} else {
d += 2 * (px - py) + 5;
py--;
}
px++;
}
drawLine(x + r, y, x + width - 1 - r, y, state);
drawLine(x + r, y + height - 1, x + width - 1 - r, y + height - 1, state);
drawLine(x, y + r, x, y + height - 1 - r, state);
drawLine(x + width - 1, y + r, x + width - 1, y + height - 1 - r, state);
}
void GfxRenderer::fillRoundedRect(const int x, const int y, const int width,
const int height, const int radius, const bool state) const {
if (radius <= 0) {
fillRect(x, y, width, height, state);
return;
}
int r = std::min(radius, std::min(width / 2, height / 2));
fillRect(x + r, y, width - 2 * r, height, state);
fillRect(x, y + r, r, height - 2 * r, state);
fillRect(x + width - r, y + r, r, height - 2 * r, state);
int px = 0, py = r;
int d = 1 - r;
while (px <= py) {
drawLine(x + r - py, y + r - px, x + r, y + r - px, state);
drawLine(x + width - 1 - r, y + r - px, x + width - 1 - r + py, y + r - px, state);
drawLine(x + r - px, y + r - py, x + r, y + r - py, state);
drawLine(x + width - 1 - r, y + r - py, x + width - 1 - r + px, y + r - py, state);
drawLine(x + r - py, y + height - 1 - r + px, x + r, y + height - 1 - r + px, state);
drawLine(x + width - 1 - r, y + height - 1 - r + px, x + width - 1 - r + py, y + height - 1 - r + px, state);
drawLine(x + r - px, y + height - 1 - r + py, x + r, y + height - 1 - r + py, state);
drawLine(x + width - 1 - r, y + height - 1 - r + py, x + width - 1 - r + px, y + height - 1 - r + py, state);
if (d < 0) {
d += 2 * px + 3;
} else {
d += 2 * (px - py) + 5;
py--;
}
px++;
}
}
void GfxRenderer::fillRoundedRectDithered(const int x, const int y, const int width,
const int height, const int radius,
const uint8_t grayLevel) const {
if (grayLevel == 0x00) {
fillRoundedRect(x, y, width, height, radius, true);
return;
}
if (grayLevel >= 0xF0) {
fillRoundedRect(x, y, width, height, radius, false);
return;
}
int r = std::min(radius, std::min(width / 2, height / 2));
if (r <= 0) {
fillRectDithered(x, y, width, height, grayLevel);
return;
}
const int screenWidth = getScreenWidth();
const int screenHeight = getScreenHeight();
int threshold = (grayLevel * 4) / 255;
auto isInside = [&](int px, int py) -> bool {
if (px < x + r && py < y + r) {
int dx = px - (x + r);
int dy = py - (y + r);
return (dx * dx + dy * dy) <= r * r;
}
if (px >= x + width - r && py < y + r) {
int dx = px - (x + width - 1 - r);
int dy = py - (y + r);
return (dx * dx + dy * dy) <= r * r;
}
if (px < x + r && py >= y + height - r) {
int dx = px - (x + r);
int dy = py - (y + height - 1 - r);
return (dx * dx + dy * dy) <= r * r;
}
if (px >= x + width - r && py >= y + height - r) {
int dx = px - (x + width - 1 - r);
int dy = py - (y + height - 1 - r);
return (dx * dx + dy * dy) <= r * r;
}
return px >= x && px < x + width && py >= y && py < y + height;
};
const int x1 = std::max(0, x);
const int y1 = std::max(0, y);
const int x2 = std::min(screenWidth - 1, x + width - 1);
const int y2 = std::min(screenHeight - 1, y + height - 1);
for (int sy = y1; sy <= y2; sy++) {
for (int sx = x1; sx <= x2; sx++) {
if (!isInside(sx, sy)) continue;
int bayerValue;
int bx = sx % 2;
int by = sy % 2;
if (bx == 0 && by == 0) bayerValue = 0;
else if (bx == 1 && by == 0) bayerValue = 2;
else if (bx == 0 && by == 1) bayerValue = 3;
else bayerValue = 1;
bool isBlack = bayerValue >= threshold;
drawPixel(sx, sy, isBlack);
}
}
}
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
int rotatedX = 0;
int rotatedY = 0;
@ -384,7 +600,7 @@ void GfxRenderer::drawBitmap(const Bitmap &bitmap, const int x, const int y,
void GfxRenderer::draw2BitImage(const uint8_t data[], int x, int y, int w,
int h) const {
uint8_t *frameBuffer = einkDisplay.getFrameBuffer();
uint8_t *frameBuffer = display.getFrameBuffer();
if (!frameBuffer) {
return;
}
@ -418,9 +634,9 @@ void GfxRenderer::draw2BitImage(const uint8_t data[], int x, int y, int w,
// val < 3 means black pixel in 2-bit representation
if (val < 3) {
// In Portrait: physical Y = DISPLAY_HEIGHT - 1 - screenX
const int physY = EInkDisplay::DISPLAY_HEIGHT - 1 - screenX;
const int physY = HalDisplay::DISPLAY_HEIGHT - 1 - screenX;
const uint16_t byteIndex =
physY * EInkDisplay::DISPLAY_WIDTH_BYTES + (physX / 8);
physY * HalDisplay::DISPLAY_WIDTH_BYTES + (physX / 8);
const uint8_t bitPosition = 7 - (physX % 8);
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit = black
}
@ -448,10 +664,10 @@ void GfxRenderer::draw2BitImage(const uint8_t data[], int x, int y, int w,
if (val < 3) {
// PortraitInverted: physical X = DISPLAY_WIDTH - 1 - screenY
// physical Y = screenX
const int physX = EInkDisplay::DISPLAY_WIDTH - 1 - screenY;
const int physX = HalDisplay::DISPLAY_WIDTH - 1 - screenY;
const int physY = screenX;
const uint16_t byteIndex =
physY * EInkDisplay::DISPLAY_WIDTH_BYTES + (physX / 8);
physY * HalDisplay::DISPLAY_WIDTH_BYTES + (physX / 8);
const uint8_t bitPosition = 7 - (physX % 8);
frameBuffer[byteIndex] &= ~(1 << bitPosition);
}
@ -481,14 +697,14 @@ void GfxRenderer::draw2BitImage(const uint8_t data[], int x, int y, int w,
if (val < 3) {
int physX, physY;
if (orientation == LandscapeClockwise) {
physX = EInkDisplay::DISPLAY_WIDTH - 1 - screenX;
physY = EInkDisplay::DISPLAY_HEIGHT - 1 - screenY;
physX = HalDisplay::DISPLAY_WIDTH - 1 - screenX;
physY = HalDisplay::DISPLAY_HEIGHT - 1 - screenY;
} else {
physX = screenX;
physY = screenY;
}
const uint16_t byteIndex =
physY * EInkDisplay::DISPLAY_WIDTH_BYTES + (physX / 8);
physY * HalDisplay::DISPLAY_WIDTH_BYTES + (physX / 8);
const uint8_t bitPosition = 7 - (physX % 8);
frameBuffer[byteIndex] &= ~(1 << bitPosition);
}

View File

@ -66,9 +66,14 @@ public:
// Drawing
void drawPixel(int x, int y, bool state = true) const;
bool readPixel(int x, int y) const; // Returns true if pixel is black
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
void drawRect(int x, int y, int width, int height, bool state = true) const;
void fillRect(int x, int y, int width, int height, bool state = true) const;
void fillRectDithered(int x, int y, int width, int height, uint8_t grayLevel) const;
void drawRoundedRect(int x, int y, int width, int height, int radius, bool state = true) const;
void fillRoundedRect(int x, int y, int width, int height, int radius, bool state = true) const;
void fillRoundedRectDithered(int x, int y, int width, int height, int radius, uint8_t grayLevel) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width,
int height) const;
void drawBitmap(const Bitmap &bitmap, int x, int y, int maxWidth,

View File

@ -17,6 +17,8 @@ protected:
bool hasBg = false;
bool border = false;
Expression borderExpr; // Dynamic border based on expression
int padding = 0; // Inner padding for children
int borderRadius = 0; // Corner radius (for future rounded rect support)
public:
Container(const std::string &id) : UIElement(id) {
@ -53,11 +55,30 @@ public:
bool hasBorderExpr() const { return !borderExpr.empty(); }
void setPadding(int p) {
padding = p;
markDirty();
}
int getPadding() const { return padding; }
void setBorderRadius(int r) {
borderRadius = r;
markDirty();
}
int getBorderRadius() const { return borderRadius; }
void layout(const ThemeContext &context, int parentX, int parentY,
int parentW, int parentH) override {
UIElement::layout(context, parentX, parentY, parentW, parentH);
// Children are laid out with padding offset
int childX = absX + padding;
int childY = absY + padding;
int childW = absW - 2 * padding;
int childH = absH - 2 * padding;
for (auto child : children) {
child->layout(context, absX, absY, absW, absH);
child->layout(context, childX, childY, childW, childH);
}
}
@ -75,7 +96,27 @@ public:
if (hasBg) {
std::string colStr = context.evaluatestring(bgColorExpr);
uint8_t color = Color::parse(colStr).value;
renderer.fillRect(absX, absY, absW, absH, color == 0x00);
// Use dithered fill for grayscale values, solid fill for black/white
// Use rounded rect if borderRadius > 0
if (color == 0x00) {
if (borderRadius > 0) {
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, true);
} else {
renderer.fillRect(absX, absY, absW, absH, true);
}
} else if (color >= 0xF0) {
if (borderRadius > 0) {
renderer.fillRoundedRect(absX, absY, absW, absH, borderRadius, false);
} else {
renderer.fillRect(absX, absY, absW, absH, false);
}
} else {
if (borderRadius > 0) {
renderer.fillRoundedRectDithered(absX, absY, absW, absH, borderRadius, color);
} else {
renderer.fillRectDithered(absX, absY, absW, absH, color);
}
}
}
// Handle dynamic border expression
@ -85,8 +126,12 @@ public:
}
if (drawBorder) {
if (borderRadius > 0) {
renderer.drawRoundedRect(absX, absY, absW, absH, borderRadius, true);
} else {
renderer.drawRect(absX, absY, absW, absH, true);
}
}
for (auto child : children) {
child->draw(renderer, context);

View File

@ -77,9 +77,9 @@ Height = 18
Parent = Home
Type = Container
X = 0
Y = 45
Y = 30
Width = 100%
Height = 260
Height = 280
Visible = {HasRecentBooks}
[RecentBooksList]
@ -87,50 +87,45 @@ Parent = RecentBooksSection
Type = List
Source = RecentBooks
ItemTemplate = RecentBookItem
X = 15
X = 10
Y = 0
Width = 450
Height = 260
Width = 460
Height = 280
Direction = Horizontal
ItemWidth = 145
Spacing = 10
ItemWidth = 149
ItemHeight = 270
Spacing = 8
; --- Recent Book Item Template ---
; Based on mockup: 74.5px at 240px scale = 149px at 480px
[RecentBookItem]
Type = Container
Width = 145
Height = 250
[BookCoverContainer]
Parent = RecentBookItem
Type = Container
X = 0
Y = 0
Width = 145
Height = 195
Border = {Item.Selected}
Width = 149
Height = 270
Padding = 8
BgColor = {Item.Selected ? "0xD9" : "white"}
BorderRadius = 12
[BookCoverImage]
Parent = BookCoverContainer
Parent = RecentBookItem
Type = Bitmap
X = 2
Y = 2
Width = 141
Height = 191
X = 0
Y = 0
Width = 133
Height = 190
Src = {Item.Image}
Cacheable = true
[BookProgressBadge]
Parent = BookCoverContainer
Parent = RecentBookItem
Type = Badge
X = 5
Y = 168
X = 4
Y = 224
Text = {Item.Progress}%
Font = UI_10
BgColor = black
FgColor = white
PaddingH = 6
PaddingV = 2
PaddingV = 3
[BookTitleLabel]
Parent = RecentBookItem
@ -138,9 +133,9 @@ Type = Label
Font = UI_10
Text = {Item.Title}
X = 0
Y = 200
Width = 145
Height = 40
Y = 196
Width = 133
Height = 22
Ellipsis = true
; --- No Recent Books State ---
@ -189,7 +184,9 @@ Width = 210
Height = 65
Spacing = 12
CenterVertical = true
Border = {Item.Selected}
Padding = 16
BgColor = {Item.Selected ? "0xD9" : "white"}
BorderRadius = 12
[MenuItemIcon]
Parent = MainMenuItem

View File

@ -20,16 +20,26 @@ void BitmapElement::draw(const GfxRenderer &renderer,
return;
}
// 1. Try Processed Cache in ThemeManager (keyed by path + target dimensions)
// Check if we have a cached 1-bit render of this bitmap at this size
const ProcessedAsset *processed =
ThemeManager::get().getProcessedAsset(path, renderer.getOrientation(), absW, absH);
if (processed && processed->w == absW && processed->h == absH) {
renderer.draw2BitImage(processed->data.data(), absX, absY, absW, absH);
// Draw cached 1-bit data directly
const int rowBytes = (absW + 7) / 8;
for (int y = 0; y < absH; y++) {
const uint8_t *srcRow = processed->data.data() + y * rowBytes;
for (int x = 0; x < absW; x++) {
bool isBlack = !(srcRow[x / 8] & (1 << (7 - (x % 8))));
if (isBlack) {
renderer.drawPixel(absX + x, absY + y, true);
}
}
}
markClean();
return;
}
// 2. Try raw asset cache, then process and cache
// Load raw asset from cache (file data cached in memory)
const std::vector<uint8_t> *data = ThemeManager::get().getCachedAsset(path);
if (!data || data->empty()) {
markClean();
@ -45,38 +55,28 @@ void BitmapElement::draw(const GfxRenderer &renderer,
// Draw the bitmap (handles scaling internally)
renderer.drawBitmap(bmp, absX, absY, absW, absH);
// After drawing, capture the rendered region and cache it for next time
// Cache the result as 1-bit packed data for next time
ProcessedAsset asset;
asset.w = absW;
asset.h = absH;
asset.orientation = renderer.getOrientation();
// Capture the rendered region from framebuffer
uint8_t *frameBuffer = renderer.getFrameBuffer();
if (frameBuffer) {
const int screenW = renderer.getScreenWidth();
const int bytesPerRow = (absW + 3) / 4;
asset.data.resize(bytesPerRow * absH);
const int rowBytes = (absW + 7) / 8;
asset.data.resize(rowBytes * absH, 0xFF); // Initialize to white
// Capture pixels using renderer's coordinate system
for (int y = 0; y < absH; y++) {
int srcOffset = ((absY + y) * screenW + absX) / 4;
int dstOffset = y * bytesPerRow;
// Copy 2-bit packed pixels
uint8_t *dstRow = asset.data.data() + y * rowBytes;
for (int x = 0; x < absW; x++) {
int sx = absX + x;
int srcByteIdx = ((absY + y) * screenW + sx) / 4;
int srcBitIdx = (sx % 4) * 2;
int dstByteIdx = dstOffset + x / 4;
int dstBitIdx = (x % 4) * 2;
uint8_t pixel = (frameBuffer[srcByteIdx] >> (6 - srcBitIdx)) & 0x03;
asset.data[dstByteIdx] &= ~(0x03 << (6 - dstBitIdx));
asset.data[dstByteIdx] |= (pixel << (6 - dstBitIdx));
// Read pixel from framebuffer (this handles orientation)
bool isBlack = renderer.readPixel(absX + x, absY + y);
if (isBlack) {
dstRow[x / 8] &= ~(1 << (7 - (x % 8)));
}
}
}
ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH);
}
markClean();
}

View File

@ -128,15 +128,21 @@ void ThemeManager::applyProperties(
}
}
} else if (key == "Padding") {
if (elemType == UIElement::ElementType::HStack) {
static_cast<HStack *>(elem)->setPadding(std::stoi(val));
} else if (elemType == UIElement::ElementType::VStack) {
static_cast<VStack *>(elem)->setPadding(std::stoi(val));
} else if (elemType == UIElement::ElementType::Grid) {
static_cast<Grid *>(elem)->setPadding(std::stoi(val));
if (elemType == UIElement::ElementType::Container ||
elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::VStack ||
elemType == UIElement::ElementType::Grid) {
static_cast<Container *>(elem)->setPadding(std::stoi(val));
} else if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar *>(elem)->setPadding(std::stoi(val));
}
} else if (key == "BorderRadius") {
if (elemType == UIElement::ElementType::Container ||
elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::VStack ||
elemType == UIElement::ElementType::Grid) {
static_cast<Container *>(elem)->setBorderRadius(std::stoi(val));
}
} else if (key == "Spacing") {
if (elemType == UIElement::ElementType::HStack) {
static_cast<HStack *>(elem)->setSpacing(std::stoi(val));
@ -283,6 +289,11 @@ void ThemeManager::applyProperties(
static_cast<ProgressBar *>(elem)->setBgColor(val);
} else if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge *>(elem)->setBgColor(val);
} else if (elemType == UIElement::ElementType::Container ||
elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::VStack ||
elemType == UIElement::ElementType::Grid) {
static_cast<Container *>(elem)->setBackgroundColorExpr(val);
}
} else if (key == "ShowBorder") {
if (elemType == UIElement::ElementType::ProgressBar) {

View File

@ -25,11 +25,9 @@ void HomeActivity::taskTrampoline(void *param) {
}
int HomeActivity::getMenuItemCount() const {
int count = 4; // My Library, Files, Transfer, Settings
if (hasContinueReading)
count++;
int count = 4; // Books, Files, Transfer, Settings
if (hasOpdsUrl)
count++;
count++; // + Calibre Library
return count;
}
@ -123,6 +121,7 @@ void HomeActivity::loadRecentBooksData() {
for (int i = 0; i < recentCount; i++) {
const std::string &bookPath = recentBooks[i];
CachedBookInfo info;
info.path = bookPath; // Store the full path
// Extract title from path
info.title = bookPath;
@ -253,28 +252,32 @@ void HomeActivity::freeCoverBuffer() {
}
void HomeActivity::loop() {
const bool prevPressed =
mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left);
const bool nextPressed =
mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right);
const bool upPressed = mappedInput.wasPressed(MappedInputManager::Button::Up);
const bool downPressed = mappedInput.wasPressed(MappedInputManager::Button::Down);
const bool leftPressed = mappedInput.wasPressed(MappedInputManager::Button::Left);
const bool rightPressed = mappedInput.wasPressed(MappedInputManager::Button::Right);
const bool confirmPressed = mappedInput.wasReleased(MappedInputManager::Button::Confirm);
const int bookCount = static_cast<int>(cachedRecentBooks.size());
const int menuCount = getMenuItemCount();
const bool hasBooks = bookCount > 0;
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Calculate dynamic indices based on which options are available
if (confirmPressed) {
if (inBookSelection && hasBooks && bookSelectorIndex < bookCount) {
// Open selected book - set the path and trigger continue reading
APP_STATE.openEpubPath = cachedRecentBooks[bookSelectorIndex].path;
onContinueReading();
return;
} else if (!inBookSelection) {
// Menu selection - calculate which action
int idx = 0;
const int continueIdx = hasContinueReading ? idx++ : -1;
const int myLibraryIdx = idx++;
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
const int filesIdx = idx++;
const int transferIdx = idx++;
const int settingsIdx = idx;
if (selectorIndex == continueIdx) {
onContinueReading();
} else if (selectorIndex == myLibraryIdx) {
if (selectorIndex == myLibraryIdx) {
onMyLibraryOpen();
} else if (selectorIndex == opdsLibraryIdx) {
onOpdsBrowserOpen();
@ -285,12 +288,50 @@ void HomeActivity::loop() {
} else if (selectorIndex == settingsIdx) {
onSettingsOpen();
}
} else if (prevPressed) {
selectorIndex = (selectorIndex + menuCount - 1) % menuCount;
}
return;
}
if (inBookSelection && hasBooks) {
// Book selection mode
if (leftPressed) {
bookSelectorIndex = (bookSelectorIndex + bookCount - 1) % bookCount;
updateRequired = true;
} else if (nextPressed) {
selectorIndex = (selectorIndex + 1) % menuCount;
} else if (rightPressed) {
bookSelectorIndex = (bookSelectorIndex + 1) % bookCount;
updateRequired = true;
} else if (downPressed) {
// Move to menu selection
inBookSelection = false;
selectorIndex = 0;
updateRequired = true;
}
} else {
// Menu selection mode
if (upPressed) {
if (selectorIndex == 0 && hasBooks) {
// Move back to book selection
inBookSelection = true;
updateRequired = true;
} else if (selectorIndex > 0) {
selectorIndex--;
updateRequired = true;
}
} else if (downPressed) {
if (selectorIndex < menuCount - 1) {
selectorIndex++;
updateRequired = true;
}
} else if (leftPressed || rightPressed) {
// In menu, left/right can also navigate (for 2-column layout)
if (leftPressed && selectorIndex > 0) {
selectorIndex--;
updateRequired = true;
} else if (rightPressed && selectorIndex < menuCount - 1) {
selectorIndex++;
updateRequired = true;
}
}
}
}
@ -316,16 +357,8 @@ void HomeActivity::render() {
lastBatteryCheck = now;
}
// Optimization: If we have a cached framebuffer from a previous full render,
// and only the selection changed (no battery update), restore it first.
const bool canRestoreBuffer = coverBufferStored && coverRendered;
const bool isSelectionOnlyChange = !needBatteryUpdate && canRestoreBuffer;
if (isSelectionOnlyChange) {
restoreCoverBuffer();
} else {
// Always clear screen - ThemeEngine handles caching internally
renderer.clearScreen();
}
ThemeEngine::ThemeContext context;
@ -348,7 +381,8 @@ void HomeActivity::render() {
context.setString(prefix + "Title", book.title);
context.setString(prefix + "Image", book.coverPath);
context.setString(prefix + "Progress", std::to_string(book.progressPercent));
context.setBool(prefix + "Selected", false);
// Book is selected if we're in book selection mode and this is the selected index
context.setBool(prefix + "Selected", inBookSelection && i == bookSelectorIndex);
}
// --- Book Card Data (for legacy theme) ---
@ -360,43 +394,46 @@ void HomeActivity::render() {
hasContinueReading && hasCoverImage && !coverBmpPath.empty());
context.setBool("ShowInfoBox", true);
// --- Selection Logic ---
// --- Selection Logic (for menu items, books handled separately) ---
int idx = 0;
const int continueIdx = hasContinueReading ? idx++ : -1;
const int myLibraryIdx = idx++;
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
const int filesIdx = idx++;
const int transferIdx = idx++;
const int settingsIdx = idx;
context.setBool("IsBookSelected", selectorIndex == continueIdx);
// IsBookSelected is true when we're in book selection mode
context.setBool("IsBookSelected", inBookSelection);
// --- Main Menu Data ---
std::vector<std::string> menuLabels;
std::vector<std::string> menuIcons;
std::vector<bool> menuSelected;
// Menu items are only selected when NOT in book selection mode
const bool menuActive = !inBookSelection;
menuLabels.push_back("Books");
menuIcons.push_back("book");
menuSelected.push_back(selectorIndex == myLibraryIdx);
menuSelected.push_back(menuActive && selectorIndex == myLibraryIdx);
if (hasOpdsUrl) {
menuLabels.push_back("OPDS Browser");
menuIcons.push_back("library");
menuSelected.push_back(selectorIndex == opdsLibraryIdx);
menuSelected.push_back(menuActive && selectorIndex == opdsLibraryIdx);
}
menuLabels.push_back("Files");
menuIcons.push_back("folder");
menuSelected.push_back(selectorIndex == filesIdx);
menuSelected.push_back(menuActive && selectorIndex == filesIdx);
menuLabels.push_back("Transfer");
menuIcons.push_back("transfer");
menuSelected.push_back(selectorIndex == transferIdx);
menuSelected.push_back(menuActive && selectorIndex == transferIdx);
menuLabels.push_back("Settings");
menuIcons.push_back("settings");
menuSelected.push_back(selectorIndex == settingsIdx);
menuSelected.push_back(menuActive && selectorIndex == settingsIdx);
context.setInt("MainMenu.Count", menuLabels.size());
for (size_t i = 0; i < menuLabels.size(); ++i) {

View File

@ -11,6 +11,7 @@
// Cached data for a recent book
struct CachedBookInfo {
std::string path; // Full path to the book file
std::string title;
std::string coverPath;
int progressPercent = 0;
@ -19,7 +20,9 @@ struct CachedBookInfo {
class HomeActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int selectorIndex = 0;
int selectorIndex = 0; // Menu item index
int bookSelectorIndex = 0; // Book selection index (0-2 for recent books)
bool inBookSelection = true; // True = selecting books, False = selecting menu
bool updateRequired = false;
bool hasContinueReading = false;
bool hasOpdsUrl = false;

View File

@ -114,9 +114,9 @@ Height = 18
Parent = Home
Type = Container
X = 0
Y = 45
Y = 30
Width = 100%
Height = 260
Height = 280
Visible = {HasRecentBooks}
[RecentBooksList]
@ -124,50 +124,45 @@ Parent = RecentBooksSection
Type = List
Source = RecentBooks
ItemTemplate = RecentBookItem
X = 15
X = 10
Y = 0
Width = 450
Height = 260
Width = 460
Height = 280
Direction = Horizontal
ItemWidth = 145
Spacing = 10
ItemWidth = 149
ItemHeight = 270
Spacing = 8
; --- Recent Book Item Template ---
; Based on mockup: 74.5px at 240px scale = 149px at 480px
[RecentBookItem]
Type = Container
Width = 145
Height = 250
[BookCoverContainer]
Parent = RecentBookItem
Type = Container
X = 0
Y = 0
Width = 145
Height = 195
Border = {Item.Selected}
Width = 149
Height = 270
Padding = 8
BgColor = {Item.Selected ? "0xD9" : "white"}
BorderRadius = 12
[BookCoverImage]
Parent = BookCoverContainer
Parent = RecentBookItem
Type = Bitmap
X = 2
Y = 2
Width = 141
Height = 191
X = 0
Y = 0
Width = 133
Height = 190
Src = {Item.Image}
Cacheable = true
[BookProgressBadge]
Parent = BookCoverContainer
Parent = RecentBookItem
Type = Badge
X = 5
Y = 168
X = 4
Y = 224
Text = {Item.Progress}%
Font = UI_10
BgColor = black
FgColor = white
PaddingH = 6
PaddingV = 2
PaddingV = 3
[BookTitleLabel]
Parent = RecentBookItem
@ -175,9 +170,9 @@ Type = Label
Font = UI_10
Text = {Item.Title}
X = 0
Y = 200
Width = 145
Height = 40
Y = 196
Width = 133
Height = 22
Ellipsis = true
; --- No Recent Books State ---
@ -226,7 +221,9 @@ Width = 210
Height = 65
Spacing = 12
CenterVertical = true
Border = {Item.Selected}
Padding = 16
BgColor = {Item.Selected ? "0xD9" : "white"}
BorderRadius = 12
[MenuItemIcon]
Parent = MainMenuItem