diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index a194f740..7d62e33c 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -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); } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 6518c1db..bfc9b6a8 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -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, diff --git a/lib/ThemeEngine/include/BasicElements.h b/lib/ThemeEngine/include/BasicElements.h index c7b23448..5bf1f594 100644 --- a/lib/ThemeEngine/include/BasicElements.h +++ b/lib/ThemeEngine/include/BasicElements.h @@ -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) { @@ -52,12 +54,31 @@ 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,7 +126,11 @@ public: } if (drawBorder) { - renderer.drawRect(absX, absY, absW, absH, true); + if (borderRadius > 0) { + renderer.drawRoundedRect(absX, absY, absW, absH, borderRadius, true); + } else { + renderer.drawRect(absX, absY, absW, absH, true); + } } for (auto child : children) { diff --git a/lib/ThemeEngine/include/DefaultTheme.h b/lib/ThemeEngine/include/DefaultTheme.h index da59424b..192bbda3 100644 --- a/lib/ThemeEngine/include/DefaultTheme.h +++ b/lib/ThemeEngine/include/DefaultTheme.h @@ -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 diff --git a/lib/ThemeEngine/src/BasicElements.cpp b/lib/ThemeEngine/src/BasicElements.cpp index 8ea71a36..1f180076 100644 --- a/lib/ThemeEngine/src/BasicElements.cpp +++ b/lib/ThemeEngine/src/BasicElements.cpp @@ -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 *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); - - for (int y = 0; y < absH; y++) { - int srcOffset = ((absY + y) * screenW + absX) / 4; - int dstOffset = y * bytesPerRow; - // Copy 2-bit packed pixels - 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)); + 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++) { + uint8_t *dstRow = asset.data.data() + y * rowBytes; + for (int x = 0; x < absW; x++) { + // 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); } + + ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH); markClean(); } diff --git a/lib/ThemeEngine/src/ThemeManager.cpp b/lib/ThemeEngine/src/ThemeManager.cpp index c18fec94..4f7651b9 100644 --- a/lib/ThemeEngine/src/ThemeManager.cpp +++ b/lib/ThemeEngine/src/ThemeManager.cpp @@ -128,15 +128,21 @@ void ThemeManager::applyProperties( } } } else if (key == "Padding") { - if (elemType == UIElement::ElementType::HStack) { - static_cast(elem)->setPadding(std::stoi(val)); - } else if (elemType == UIElement::ElementType::VStack) { - static_cast(elem)->setPadding(std::stoi(val)); - } else if (elemType == UIElement::ElementType::Grid) { - static_cast(elem)->setPadding(std::stoi(val)); + if (elemType == UIElement::ElementType::Container || + elemType == UIElement::ElementType::HStack || + elemType == UIElement::ElementType::VStack || + elemType == UIElement::ElementType::Grid) { + static_cast(elem)->setPadding(std::stoi(val)); } else if (elemType == UIElement::ElementType::TabBar) { static_cast(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(elem)->setBorderRadius(std::stoi(val)); + } } else if (key == "Spacing") { if (elemType == UIElement::ElementType::HStack) { static_cast(elem)->setSpacing(std::stoi(val)); @@ -283,6 +289,11 @@ void ThemeManager::applyProperties( static_cast(elem)->setBgColor(val); } else if (elemType == UIElement::ElementType::Badge) { static_cast(elem)->setBgColor(val); + } else if (elemType == UIElement::ElementType::Container || + elemType == UIElement::ElementType::HStack || + elemType == UIElement::ElementType::VStack || + elemType == UIElement::ElementType::Grid) { + static_cast(elem)->setBackgroundColorExpr(val); } } else if (key == "ShowBorder") { if (elemType == UIElement::ElementType::ProgressBar) { diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index f8f8784b..d30d1e5a 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -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,44 +252,86 @@ 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(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 - 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) { + if (confirmPressed) { + if (inBookSelection && hasBooks && bookSelectorIndex < bookCount) { + // Open selected book - set the path and trigger continue reading + APP_STATE.openEpubPath = cachedRecentBooks[bookSelectorIndex].path; onContinueReading(); - } else if (selectorIndex == myLibraryIdx) { - onMyLibraryOpen(); - } else if (selectorIndex == opdsLibraryIdx) { - onOpdsBrowserOpen(); - } else if (selectorIndex == filesIdx) { - onMyLibraryOpen(); // Files = file browser - } else if (selectorIndex == transferIdx) { - onFileTransferOpen(); // Transfer = web transfer - } else if (selectorIndex == settingsIdx) { - onSettingsOpen(); + return; + } else if (!inBookSelection) { + // Menu selection - calculate which action + int idx = 0; + const int myLibraryIdx = idx++; + const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1; + const int filesIdx = idx++; + const int transferIdx = idx++; + const int settingsIdx = idx; + + if (selectorIndex == myLibraryIdx) { + onMyLibraryOpen(); + } else if (selectorIndex == opdsLibraryIdx) { + onOpdsBrowserOpen(); + } else if (selectorIndex == filesIdx) { + onMyLibraryOpen(); // Files = file browser + } else if (selectorIndex == transferIdx) { + onFileTransferOpen(); // Transfer = web transfer + } else if (selectorIndex == settingsIdx) { + onSettingsOpen(); + } + } + return; + } + + if (inBookSelection && hasBooks) { + // Book selection mode + if (leftPressed) { + bookSelectorIndex = (bookSelectorIndex + bookCount - 1) % bookCount; + updateRequired = true; + } 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; + } } - } else if (prevPressed) { - selectorIndex = (selectorIndex + menuCount - 1) % menuCount; - updateRequired = true; - } else if (nextPressed) { - selectorIndex = (selectorIndex + 1) % menuCount; - 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 { - renderer.clearScreen(); - } + // 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 menuLabels; std::vector menuIcons; std::vector 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) { diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 235a652c..9b59f409 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -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; diff --git a/src/activities/home/default.ini b/src/activities/home/default.ini index 4dd27819..6487e381 100644 --- a/src/activities/home/default.ini +++ b/src/activities/home/default.ini @@ -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