mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
- rounded rects
- background fill - border radius - container paddings - fix navigation in home
This commit is contained in:
parent
d54f3c5143
commit
7a5c1e8e0e
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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<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
|
||||
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<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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user