fix: reset clang correctly

This commit is contained in:
Brackyt 2026-01-25 17:48:57 +01:00
parent bf353e7d83
commit f6469c48ba
26 changed files with 1298 additions and 1746 deletions

View File

@ -1,8 +1,10 @@
#include "Bitmap.h" #include "Bitmap.h"
#include "BitmapHelpers.h"
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include "BitmapHelpers.h"
// ============================================================================ // ============================================================================
// IMAGE PROCESSING OPTIONS // IMAGE PROCESSING OPTIONS
// ============================================================================ // ============================================================================
@ -31,13 +33,12 @@ int Bitmap::readByte() const {
return -1; return -1;
} }
size_t Bitmap::readBytes(void *buf, size_t count) const { size_t Bitmap::readBytes(void* buf, size_t count) const {
if (file && *file) { if (file && *file) {
return file->read(buf, count); return file->read(buf, count);
} else if (memoryBuffer) { } else if (memoryBuffer) {
size_t available = memorySize - bufferPos; size_t available = memorySize - bufferPos;
if (count > available) if (count > available) count = available;
count = available;
memcpy(buf, memoryBuffer + bufferPos, count); memcpy(buf, memoryBuffer + bufferPos, count);
bufferPos += count; bufferPos += count;
return count; return count;
@ -74,8 +75,7 @@ bool Bitmap::seekCur(int32_t offset) const {
uint16_t Bitmap::readLE16() { uint16_t Bitmap::readLE16() {
const int c0 = readByte(); const int c0 = readByte();
const int c1 = readByte(); const int c1 = readByte();
return static_cast<uint16_t>(c0 & 0xFF) | return static_cast<uint16_t>(c0 & 0xFF) | (static_cast<uint16_t>(c1 & 0xFF) << 8);
(static_cast<uint16_t>(c1 & 0xFF) << 8);
} }
uint32_t Bitmap::readLE32() { uint32_t Bitmap::readLE32() {
@ -83,64 +83,58 @@ uint32_t Bitmap::readLE32() {
const int c1 = readByte(); const int c1 = readByte();
const int c2 = readByte(); const int c2 = readByte();
const int c3 = readByte(); const int c3 = readByte();
return static_cast<uint32_t>(c0 & 0xFF) | return static_cast<uint32_t>(c0 & 0xFF) | (static_cast<uint32_t>(c1 & 0xFF) << 8) |
(static_cast<uint32_t>(c1 & 0xFF) << 8) | (static_cast<uint32_t>(c2 & 0xFF) << 16) | (static_cast<uint32_t>(c3 & 0xFF) << 24);
(static_cast<uint32_t>(c2 & 0xFF) << 16) |
(static_cast<uint32_t>(c3 & 0xFF) << 24);
} }
const char *Bitmap::errorToString(BmpReaderError err) { const char* Bitmap::errorToString(BmpReaderError err) {
switch (err) { switch (err) {
case BmpReaderError::Ok: case BmpReaderError::Ok:
return "Ok"; return "Ok";
case BmpReaderError::FileInvalid: case BmpReaderError::FileInvalid:
return "FileInvalid"; return "FileInvalid";
case BmpReaderError::SeekStartFailed: case BmpReaderError::SeekStartFailed:
return "SeekStartFailed"; return "SeekStartFailed";
case BmpReaderError::NotBMP: case BmpReaderError::NotBMP:
return "NotBMP"; return "NotBMP";
case BmpReaderError::DIBTooSmall: case BmpReaderError::DIBTooSmall:
return "DIBTooSmall"; return "DIBTooSmall";
case BmpReaderError::BadPlanes: case BmpReaderError::BadPlanes:
return "BadPlanes"; return "BadPlanes";
case BmpReaderError::UnsupportedBpp: case BmpReaderError::UnsupportedBpp:
return "UnsupportedBpp"; return "UnsupportedBpp";
case BmpReaderError::UnsupportedCompression: case BmpReaderError::UnsupportedCompression:
return "UnsupportedCompression"; return "UnsupportedCompression";
case BmpReaderError::BadDimensions: case BmpReaderError::BadDimensions:
return "BadDimensions"; return "BadDimensions";
case BmpReaderError::ImageTooLarge: case BmpReaderError::ImageTooLarge:
return "ImageTooLarge"; return "ImageTooLarge";
case BmpReaderError::PaletteTooLarge: case BmpReaderError::PaletteTooLarge:
return "PaletteTooLarge"; return "PaletteTooLarge";
case BmpReaderError::SeekPixelDataFailed: case BmpReaderError::SeekPixelDataFailed:
return "SeekPixelDataFailed"; return "SeekPixelDataFailed";
case BmpReaderError::BufferTooSmall: case BmpReaderError::BufferTooSmall:
return "BufferTooSmall"; return "BufferTooSmall";
case BmpReaderError::OomRowBuffer: case BmpReaderError::OomRowBuffer:
return "OomRowBuffer"; return "OomRowBuffer";
case BmpReaderError::ShortReadRow: case BmpReaderError::ShortReadRow:
return "ShortReadRow"; return "ShortReadRow";
} }
return "Unknown"; return "Unknown";
} }
BmpReaderError Bitmap::parseHeaders() { BmpReaderError Bitmap::parseHeaders() {
if (!file && !memoryBuffer) if (!file && !memoryBuffer) return BmpReaderError::FileInvalid;
return BmpReaderError::FileInvalid; if (!seekSet(0)) return BmpReaderError::SeekStartFailed;
if (!seekSet(0))
return BmpReaderError::SeekStartFailed;
const uint16_t bfType = readLE16(); const uint16_t bfType = readLE16();
if (bfType != 0x4D42) if (bfType != 0x4D42) return BmpReaderError::NotBMP;
return BmpReaderError::NotBMP;
seekCur(8); seekCur(8);
bfOffBits = readLE32(); bfOffBits = readLE32();
const uint32_t biSize = readLE32(); const uint32_t biSize = readLE32();
if (biSize < 40) if (biSize < 40) return BmpReaderError::DIBTooSmall;
return BmpReaderError::DIBTooSmall;
width = static_cast<int32_t>(readLE32()); width = static_cast<int32_t>(readLE32());
const auto rawHeight = static_cast<int32_t>(readLE32()); const auto rawHeight = static_cast<int32_t>(readLE32());
@ -150,24 +144,18 @@ BmpReaderError Bitmap::parseHeaders() {
const uint16_t planes = readLE16(); const uint16_t planes = readLE16();
bpp = readLE16(); bpp = readLE16();
const uint32_t comp = readLE32(); const uint32_t comp = readLE32();
const bool validBpp = const bool validBpp = bpp == 1 || bpp == 2 || bpp == 8 || bpp == 24 || bpp == 32;
bpp == 1 || bpp == 2 || bpp == 8 || bpp == 24 || bpp == 32;
if (planes != 1) if (planes != 1) return BmpReaderError::BadPlanes;
return BmpReaderError::BadPlanes; if (!validBpp) return BmpReaderError::UnsupportedBpp;
if (!validBpp) if (!(comp == 0 || (bpp == 32 && comp == 3))) return BmpReaderError::UnsupportedCompression;
return BmpReaderError::UnsupportedBpp;
if (!(comp == 0 || (bpp == 32 && comp == 3)))
return BmpReaderError::UnsupportedCompression;
seekCur(12); seekCur(12);
const uint32_t colorsUsed = readLE32(); const uint32_t colorsUsed = readLE32();
if (colorsUsed > 256u) if (colorsUsed > 256u) return BmpReaderError::PaletteTooLarge;
return BmpReaderError::PaletteTooLarge;
seekCur(4); seekCur(4);
if (width <= 0 || height <= 0) if (width <= 0 || height <= 0) return BmpReaderError::BadDimensions;
return BmpReaderError::BadDimensions;
constexpr int MAX_IMAGE_WIDTH = 2048; constexpr int MAX_IMAGE_WIDTH = 2048;
constexpr int MAX_IMAGE_HEIGHT = 3072; constexpr int MAX_IMAGE_HEIGHT = 3072;
@ -177,8 +165,7 @@ BmpReaderError Bitmap::parseHeaders() {
rowBytes = (width * bpp + 31) / 32 * 4; rowBytes = (width * bpp + 31) / 32 * 4;
for (int i = 0; i < 256; i++) for (int i = 0; i < 256; i++) paletteLum[i] = static_cast<uint8_t>(i);
paletteLum[i] = static_cast<uint8_t>(i);
if (colorsUsed > 0) { if (colorsUsed > 0) {
for (uint32_t i = 0; i < colorsUsed; i++) { for (uint32_t i = 0; i < colorsUsed; i++) {
uint8_t rgb[4]; uint8_t rgb[4];
@ -187,8 +174,7 @@ BmpReaderError Bitmap::parseHeaders() {
} }
} }
if (!seekSet(bfOffBits)) if (!seekSet(bfOffBits)) return BmpReaderError::SeekPixelDataFailed;
return BmpReaderError::SeekPixelDataFailed;
if (bpp > 2 && dithering) { if (bpp > 2 && dithering) {
if (USE_ATKINSON) { if (USE_ATKINSON) {
@ -201,12 +187,11 @@ BmpReaderError Bitmap::parseHeaders() {
return BmpReaderError::Ok; return BmpReaderError::Ok;
} }
BmpReaderError Bitmap::readNextRow(uint8_t *data, uint8_t *rowBuffer) const { BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const {
if (readBytes(rowBuffer, rowBytes) != (size_t)rowBytes) if (readBytes(rowBuffer, rowBytes) != (size_t)rowBytes) return BmpReaderError::ShortReadRow;
return BmpReaderError::ShortReadRow;
prevRowY += 1; prevRowY += 1;
uint8_t *outPtr = data; uint8_t* outPtr = data;
uint8_t currentOutByte = 0; uint8_t currentOutByte = 0;
int bitShift = 6; int bitShift = 6;
int currentX = 0; int currentX = 0;
@ -236,46 +221,44 @@ BmpReaderError Bitmap::readNextRow(uint8_t *data, uint8_t *rowBuffer) const {
}; };
switch (bpp) { switch (bpp) {
case 32: { case 32: {
const uint8_t *p = rowBuffer; const uint8_t* p = rowBuffer;
for (int x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8; uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
packPixel(lum); packPixel(lum);
p += 4; p += 4;
}
break;
} }
break; case 24: {
} const uint8_t* p = rowBuffer;
case 24: { for (int x = 0; x < width; x++) {
const uint8_t *p = rowBuffer; uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
for (int x = 0; x < width; x++) { packPixel(lum);
uint8_t lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8; p += 3;
packPixel(lum); }
p += 3; break;
} }
break; case 8: {
} for (int x = 0; x < width; x++) packPixel(paletteLum[rowBuffer[x]]);
case 8: { break;
for (int x = 0; x < width; x++)
packPixel(paletteLum[rowBuffer[x]]);
break;
}
case 2: {
for (int x = 0; x < width; x++) {
uint8_t lum =
paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03];
packPixel(lum);
} }
break; case 2: {
} for (int x = 0; x < width; x++) {
case 1: { uint8_t lum = paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03];
for (int x = 0; x < width; x++) { packPixel(lum);
const uint8_t palIndex = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 1 : 0; }
packPixel(paletteLum[palIndex]); break;
} }
break; case 1: {
} for (int x = 0; x < width; x++) {
default: const uint8_t palIndex = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 1 : 0;
return BmpReaderError::UnsupportedBpp; packPixel(paletteLum[palIndex]);
}
break;
}
default:
return BmpReaderError::UnsupportedBpp;
} }
if (atkinsonDitherer) if (atkinsonDitherer)
@ -283,17 +266,13 @@ BmpReaderError Bitmap::readNextRow(uint8_t *data, uint8_t *rowBuffer) const {
else if (fsDitherer) else if (fsDitherer)
fsDitherer->nextRow(); fsDitherer->nextRow();
if (bitShift != 6) if (bitShift != 6) *outPtr = currentOutByte;
*outPtr = currentOutByte;
return BmpReaderError::Ok; return BmpReaderError::Ok;
} }
BmpReaderError Bitmap::rewindToData() const { BmpReaderError Bitmap::rewindToData() const {
if (!seekSet(bfOffBits)) if (!seekSet(bfOffBits)) return BmpReaderError::SeekPixelDataFailed;
return BmpReaderError::SeekPixelDataFailed; if (fsDitherer) fsDitherer->reset();
if (fsDitherer) if (atkinsonDitherer) atkinsonDitherer->reset();
fsDitherer->reset();
if (atkinsonDitherer)
atkinsonDitherer->reset();
return BmpReaderError::Ok; return BmpReaderError::Ok;
} }

View File

@ -29,18 +29,16 @@ enum class BmpReaderError : uint8_t {
}; };
class Bitmap { class Bitmap {
public: public:
static const char *errorToString(BmpReaderError err); static const char* errorToString(BmpReaderError err);
explicit Bitmap(FsFile &file, bool dithering = false) explicit Bitmap(FsFile& file, bool dithering = false) : file(&file), dithering(dithering) {}
: file(&file), dithering(dithering) {} explicit Bitmap(const uint8_t* buffer, size_t size, bool dithering = false)
explicit Bitmap(const uint8_t *buffer, size_t size, bool dithering = false) : file(nullptr), memoryBuffer(buffer), memorySize(size), dithering(dithering) {}
: file(nullptr), memoryBuffer(buffer), memorySize(size),
dithering(dithering) {}
~Bitmap(); ~Bitmap();
BmpReaderError parseHeaders(); BmpReaderError parseHeaders();
BmpReaderError readNextRow(uint8_t *data, uint8_t *rowBuffer) const; BmpReaderError readNextRow(uint8_t* data, uint8_t* rowBuffer) const;
BmpReaderError rewindToData() const; BmpReaderError rewindToData() const;
// Getters // Getters
@ -52,19 +50,19 @@ public:
bool is1Bit() const { return bpp == 1; } bool is1Bit() const { return bpp == 1; }
uint16_t getBpp() const { return bpp; } uint16_t getBpp() const { return bpp; }
private: private:
// Internal IO helpers // Internal IO helpers
int readByte() const; int readByte() const;
size_t readBytes(void *buf, size_t count) const; size_t readBytes(void* buf, size_t count) const;
bool seekSet(uint32_t pos) const; bool seekSet(uint32_t pos) const;
bool seekCur(int32_t offset) const; // Only needed for skip? bool seekCur(int32_t offset) const; // Only needed for skip?
uint16_t readLE16(); uint16_t readLE16();
uint32_t readLE32(); uint32_t readLE32();
// Source (one is valid) // Source (one is valid)
FsFile *file = nullptr; FsFile* file = nullptr;
const uint8_t *memoryBuffer = nullptr; const uint8_t* memoryBuffer = nullptr;
size_t memorySize = 0; size_t memorySize = 0;
mutable size_t bufferPos = 0; mutable size_t bufferPos = 0;
@ -78,10 +76,10 @@ private:
uint8_t paletteLum[256] = {}; uint8_t paletteLum[256] = {};
// Floyd-Steinberg dithering state (mutable for const methods) // Floyd-Steinberg dithering state (mutable for const methods)
mutable int16_t *errorCurRow = nullptr; mutable int16_t* errorCurRow = nullptr;
mutable int16_t *errorNextRow = nullptr; mutable int16_t* errorNextRow = nullptr;
mutable int prevRowY = -1; // Track row progression for error propagation mutable int prevRowY = -1; // Track row progression for error propagation
mutable AtkinsonDitherer *atkinsonDitherer = nullptr; mutable AtkinsonDitherer* atkinsonDitherer = nullptr;
mutable FloydSteinbergDitherer *fsDitherer = nullptr; mutable FloydSteinbergDitherer* fsDitherer = nullptr;
}; };

File diff suppressed because it is too large Load Diff

View File

@ -8,42 +8,37 @@
#include "Bitmap.h" #include "Bitmap.h"
class GfxRenderer { class GfxRenderer {
public: public:
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
// Logical screen orientation from the perspective of callers // Logical screen orientation from the perspective of callers
enum Orientation { enum Orientation {
Portrait, // 480x800 logical coordinates (current default) Portrait, // 480x800 logical coordinates (current default)
LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap
// top/bottom) // top/bottom)
PortraitInverted, // 480x800 logical coordinates, inverted PortraitInverted, // 480x800 logical coordinates, inverted
LandscapeCounterClockwise // 800x480 logical coordinates, native panel LandscapeCounterClockwise // 800x480 logical coordinates, native panel
// orientation // orientation
}; };
private: private:
static constexpr size_t BW_BUFFER_CHUNK_SIZE = static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
8000; // 8KB chunks to allow for non-contiguous memory static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
static constexpr size_t BW_BUFFER_NUM_CHUNKS = static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE,
EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS ==
EInkDisplay::BUFFER_SIZE,
"BW buffer chunking does not line up with display buffer size"); "BW buffer chunking does not line up with display buffer size");
EInkDisplay &einkDisplay; EInkDisplay& einkDisplay;
RenderMode renderMode; RenderMode renderMode;
Orientation orientation; Orientation orientation;
uint8_t *bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap; std::map<int, EpdFontFamily> fontMap;
void renderChar(const EpdFontFamily &fontFamily, uint32_t cp, int *x, void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
const int *y, bool pixelState,
EpdFontFamily::Style style) const; EpdFontFamily::Style style) const;
void freeBwBufferChunks(); void freeBwBufferChunks();
void rotateCoordinates(int x, int y, int *rotatedX, int *rotatedY) const; void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const;
public: public:
explicit GfxRenderer(EInkDisplay &einkDisplay) explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
: einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
~GfxRenderer() { freeBwBufferChunks(); } ~GfxRenderer() { freeBwBufferChunks(); }
static constexpr int VIEWABLE_MARGIN_TOP = 9; static constexpr int VIEWABLE_MARGIN_TOP = 9;
@ -62,8 +57,7 @@ public:
// Screen ops // Screen ops
int getScreenWidth() const; int getScreenWidth() const;
int getScreenHeight() const; int getScreenHeight() const;
void displayBuffer( void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
// EXPERIMENTAL: Windowed update - display only a rectangular region // EXPERIMENTAL: Windowed update - display only a rectangular region
void displayWindow(int x, int y, int width, int height) const; void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const; void invertScreen() const;
@ -71,7 +65,7 @@ public:
// Drawing // Drawing
void drawPixel(int x, int y, bool state = true) const; void drawPixel(int x, int y, bool state = true) const;
bool readPixel(int x, int y) const; // Returns true if pixel is black 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 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 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 fillRect(int x, int y, int width, int height, bool state = true) const;
@ -79,67 +73,55 @@ public:
void drawRoundedRect(int x, int y, int width, int height, int radius, bool state = true) 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 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 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, void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
int height) const; void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
void drawBitmap(const Bitmap &bitmap, int x, int y, int maxWidth, float cropY = 0) const;
int maxHeight, float cropX = 0, float cropY = 0) const; void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
void drawBitmap1Bit(const Bitmap &bitmap, int x, int y, int maxWidth,
int maxHeight) const;
void draw2BitImage(const uint8_t data[], int x, int y, int w, int h) const; void draw2BitImage(const uint8_t data[], int x, int y, int w, int h) const;
void fillPolygon(const int *xPoints, const int *yPoints, int numPoints, void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
bool state = true) const;
// Region caching - copies a rectangular region to/from a buffer // Region caching - copies a rectangular region to/from a buffer
// Returns allocated buffer on success, nullptr on failure. Caller owns the // Returns allocated buffer on success, nullptr on failure. Caller owns the
// memory. // memory.
uint8_t *captureRegion(int x, int y, int width, int height, uint8_t* captureRegion(int x, int y, int width, int height, size_t* outSize) const;
size_t *outSize) const;
// Restores a previously captured region. Buffer must match dimensions. // Restores a previously captured region. Buffer must match dimensions.
void restoreRegion(const uint8_t *buffer, int x, int y, int width, void restoreRegion(const uint8_t* buffer, int x, int y, int width, int height) const;
int height) const;
// Text // Text
int getTextWidth(int fontId, const char *text, int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; void drawCenteredText(int fontId, int y, const char* text, bool black = true,
void EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
drawCenteredText(int fontId, int y, const char *text, bool black = true, void drawText(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
void drawText(int fontId, int x, int y, const char *text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getSpaceWidth(int fontId) const; int getSpaceWidth(int fontId) const;
int getFontAscenderSize(int fontId) const; int getFontAscenderSize(int fontId) const;
int getLineHeight(int fontId) const; int getLineHeight(int fontId) const;
std::string std::string truncatedText(int fontId, const char* text, int maxWidth,
truncatedText(int fontId, const char *text, int maxWidth, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
// UI Components // UI Components
void drawButtonHints(int fontId, const char *btn1, const char *btn2, void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4);
const char *btn3, const char *btn4); void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const;
void drawSideButtonHints(int fontId, const char *topBtn,
const char *bottomBtn) const;
private: private:
// Helper for drawing rotated text (90 degrees clockwise, for side buttons) // Helper for drawing rotated text (90 degrees clockwise, for side buttons)
void drawTextRotated90CW( void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
int fontId, int x, int y, const char *text, bool black = true, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getTextHeight(int fontId) const; int getTextHeight(int fontId) const;
public: public:
// Grayscale functions // Grayscale functions
void setRenderMode(const RenderMode mode) { this->renderMode = mode; } void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
void copyGrayscaleLsbBuffers() const; void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const; void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const; void displayGrayBuffer() const;
bool storeBwBuffer(); // Returns true if buffer was stored successfully bool storeBwBuffer(); // Returns true if buffer was stored successfully
void restoreBwBuffer(); // Restore and free the stored buffer void restoreBwBuffer(); // Restore and free the stored buffer
void cleanupGrayscaleWithFrameBuffer() const; void cleanupGrayscaleWithFrameBuffer() const;
// Low level functions // Low level functions
uint8_t *getFrameBuffer() const; uint8_t* getFrameBuffer() const;
static size_t getBufferSize(); static size_t getBufferSize();
void grayscaleRevert() const; void grayscaleRevert() const;
void getOrientedViewableTRBL(int *outTop, int *outRight, int *outBottom, void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
int *outLeft) const;
}; };

View File

@ -1,43 +1,42 @@
#pragma once #pragma once
#include <Bitmap.h>
#include <SDCardManager.h>
#include <vector>
#include "ThemeContext.h" #include "ThemeContext.h"
#include "ThemeTypes.h" #include "ThemeTypes.h"
#include "UIElement.h" #include "UIElement.h"
#include <Bitmap.h>
#include <SDCardManager.h>
#include <vector>
namespace ThemeEngine { namespace ThemeEngine {
// --- Container --- // --- Container ---
class Container : public UIElement { class Container : public UIElement {
protected: protected:
std::vector<UIElement *> children; std::vector<UIElement*> children;
Expression bgColorExpr; Expression bgColorExpr;
bool hasBg = false; bool hasBg = false;
bool border = false; bool border = false;
Expression borderExpr; // Dynamic border based on expression Expression borderExpr; // Dynamic border based on expression
int padding = 0; // Inner padding for children int padding = 0; // Inner padding for children
int borderRadius = 0; // Corner radius (for future rounded rect support) int borderRadius = 0; // Corner radius (for future rounded rect support)
public: public:
Container(const std::string &id) : UIElement(id) { Container(const std::string& id) : UIElement(id) { bgColorExpr = Expression::parse("0xFF"); }
bgColorExpr = Expression::parse("0xFF");
}
virtual ~Container() { virtual ~Container() {
for (auto child : children) for (auto child : children) delete child;
delete child;
} }
Container *asContainer() override { return this; } Container* asContainer() override { return this; }
ElementType getType() const override { return ElementType::Container; } ElementType getType() const override { return ElementType::Container; }
void addChild(UIElement *child) { children.push_back(child); } void addChild(UIElement* child) { children.push_back(child); }
const std::vector<UIElement *> &getChildren() const { return children; } const std::vector<UIElement*>& getChildren() const { return children; }
void setBackgroundColorExpr(const std::string &expr) { void setBackgroundColorExpr(const std::string& expr) {
bgColorExpr = Expression::parse(expr); bgColorExpr = Expression::parse(expr);
hasBg = true; hasBg = true;
markDirty(); markDirty();
@ -48,29 +47,28 @@ public:
markDirty(); markDirty();
} }
void setBorderExpr(const std::string &expr) { void setBorderExpr(const std::string& expr) {
borderExpr = Expression::parse(expr); borderExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
bool hasBorderExpr() const { return !borderExpr.empty(); } bool hasBorderExpr() const { return !borderExpr.empty(); }
void setPadding(int p) { void setPadding(int p) {
padding = p; padding = p;
markDirty(); markDirty();
} }
int getPadding() const { return padding; } int getPadding() const { return padding; }
void setBorderRadius(int r) { void setBorderRadius(int r) {
borderRadius = r; borderRadius = r;
markDirty(); markDirty();
} }
int getBorderRadius() const { return borderRadius; } int getBorderRadius() const { return borderRadius; }
void layout(const ThemeContext &context, int parentX, int parentY, void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
int parentW, int parentH) override {
UIElement::layout(context, parentX, parentY, parentW, parentH); UIElement::layout(context, parentX, parentY, parentW, parentH);
// Children are laid out with padding offset // Children are laid out with padding offset
int childX = absX + padding; int childX = absX + padding;
@ -89,9 +87,8 @@ public:
} }
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
if (hasBg) { if (hasBg) {
std::string colStr = context.evaluatestring(bgColorExpr); std::string colStr = context.evaluatestring(bgColorExpr);
@ -144,13 +141,11 @@ public:
// --- Rectangle --- // --- Rectangle ---
class Rectangle : public UIElement { class Rectangle : public UIElement {
bool fill = false; bool fill = false;
Expression fillExpr; // Dynamic fill based on expression Expression fillExpr; // Dynamic fill based on expression
Expression colorExpr; Expression colorExpr;
public: public:
Rectangle(const std::string &id) : UIElement(id) { Rectangle(const std::string& id) : UIElement(id) { colorExpr = Expression::parse("0x00"); }
colorExpr = Expression::parse("0x00");
}
ElementType getType() const override { return ElementType::Rectangle; } ElementType getType() const override { return ElementType::Rectangle; }
void setFill(bool f) { void setFill(bool f) {
@ -158,19 +153,18 @@ public:
markDirty(); markDirty();
} }
void setFillExpr(const std::string &expr) { void setFillExpr(const std::string& expr) {
fillExpr = Expression::parse(expr); fillExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setColorExpr(const std::string &c) { void setColorExpr(const std::string& c) {
colorExpr = Expression::parse(c); colorExpr = Expression::parse(c);
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
std::string colStr = context.evaluatestring(colorExpr); std::string colStr = context.evaluatestring(colorExpr);
uint8_t color = Color::parse(colStr).value; uint8_t color = Color::parse(colStr).value;
@ -193,24 +187,22 @@ public:
// --- Label --- // --- Label ---
class Label : public UIElement { class Label : public UIElement {
public: public:
enum class Alignment { Left, Center, Right }; enum class Alignment { Left, Center, Right };
private: private:
Expression textExpr; Expression textExpr;
int fontId = 0; int fontId = 0;
Alignment alignment = Alignment::Left; Alignment alignment = Alignment::Left;
Expression colorExpr; Expression colorExpr;
int maxLines = 1; // For multi-line support int maxLines = 1; // For multi-line support
bool ellipsis = true; // Truncate with ... if too long bool ellipsis = true; // Truncate with ... if too long
public: public:
Label(const std::string &id) : UIElement(id) { Label(const std::string& id) : UIElement(id) { colorExpr = Expression::parse("0x00"); }
colorExpr = Expression::parse("0x00");
}
ElementType getType() const override { return ElementType::Label; } ElementType getType() const override { return ElementType::Label; }
void setText(const std::string &expr) { void setText(const std::string& expr) {
textExpr = Expression::parse(expr); textExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
@ -226,7 +218,7 @@ public:
alignment = c ? Alignment::Center : Alignment::Left; alignment = c ? Alignment::Center : Alignment::Left;
markDirty(); markDirty();
} }
void setColorExpr(const std::string &c) { void setColorExpr(const std::string& c) {
colorExpr = Expression::parse(c); colorExpr = Expression::parse(c);
markDirty(); markDirty();
} }
@ -239,9 +231,8 @@ public:
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
std::string finalText = context.evaluatestring(textExpr); std::string finalText = context.evaluatestring(textExpr);
if (finalText.empty()) { if (finalText.empty()) {
@ -288,13 +279,13 @@ class BitmapElement : public UIElement {
bool scaleToFit = true; bool scaleToFit = true;
bool preserveAspect = true; bool preserveAspect = true;
public: public:
BitmapElement(const std::string &id) : UIElement(id) { BitmapElement(const std::string& id) : UIElement(id) {
cacheable = true; // Bitmaps benefit from caching cacheable = true; // Bitmaps benefit from caching
} }
ElementType getType() const override { return ElementType::Bitmap; } ElementType getType() const override { return ElementType::Bitmap; }
void setSrc(const std::string &src) { void setSrc(const std::string& src) {
srcExpr = Expression::parse(src); srcExpr = Expression::parse(src);
invalidateCache(); invalidateCache();
} }
@ -309,41 +300,41 @@ public:
invalidateCache(); invalidateCache();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override; void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
}; };
// --- ProgressBar --- // --- ProgressBar ---
class ProgressBar : public UIElement { class ProgressBar : public UIElement {
Expression valueExpr; // Current value (0-100 or 0-max) Expression valueExpr; // Current value (0-100 or 0-max)
Expression maxExpr; // Max value (default 100) Expression maxExpr; // Max value (default 100)
Expression fgColorExpr; // Foreground color Expression fgColorExpr; // Foreground color
Expression bgColorExpr; // Background color Expression bgColorExpr; // Background color
bool showBorder = true; bool showBorder = true;
int borderWidth = 1; int borderWidth = 1;
public: public:
ProgressBar(const std::string &id) : UIElement(id) { ProgressBar(const std::string& id) : UIElement(id) {
valueExpr = Expression::parse("0"); valueExpr = Expression::parse("0");
maxExpr = Expression::parse("100"); maxExpr = Expression::parse("100");
fgColorExpr = Expression::parse("0x00"); // Black fill fgColorExpr = Expression::parse("0x00"); // Black fill
bgColorExpr = Expression::parse("0xFF"); // White background bgColorExpr = Expression::parse("0xFF"); // White background
} }
ElementType getType() const override { return ElementType::ProgressBar; } ElementType getType() const override { return ElementType::ProgressBar; }
void setValue(const std::string &expr) { void setValue(const std::string& expr) {
valueExpr = Expression::parse(expr); valueExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setMax(const std::string &expr) { void setMax(const std::string& expr) {
maxExpr = Expression::parse(expr); maxExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setFgColor(const std::string &expr) { void setFgColor(const std::string& expr) {
fgColorExpr = Expression::parse(expr); fgColorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setBgColor(const std::string &expr) { void setBgColor(const std::string& expr) {
bgColorExpr = Expression::parse(expr); bgColorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
@ -352,23 +343,19 @@ public:
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
std::string valStr = context.evaluatestring(valueExpr); std::string valStr = context.evaluatestring(valueExpr);
std::string maxStr = context.evaluatestring(maxExpr); std::string maxStr = context.evaluatestring(maxExpr);
int value = valStr.empty() ? 0 : std::stoi(valStr); int value = valStr.empty() ? 0 : std::stoi(valStr);
int maxVal = maxStr.empty() ? 100 : std::stoi(maxStr); int maxVal = maxStr.empty() ? 100 : std::stoi(maxStr);
if (maxVal <= 0) if (maxVal <= 0) maxVal = 100;
maxVal = 100;
float ratio = static_cast<float>(value) / static_cast<float>(maxVal); float ratio = static_cast<float>(value) / static_cast<float>(maxVal);
if (ratio < 0) if (ratio < 0) ratio = 0;
ratio = 0; if (ratio > 1) ratio = 1;
if (ratio > 1)
ratio = 1;
// Draw background // Draw background
std::string bgStr = context.evaluatestring(bgColorExpr); std::string bgStr = context.evaluatestring(bgColorExpr);
@ -398,14 +385,12 @@ class Divider : public UIElement {
bool horizontal = true; bool horizontal = true;
int thickness = 1; int thickness = 1;
public: public:
Divider(const std::string &id) : UIElement(id) { Divider(const std::string& id) : UIElement(id) { colorExpr = Expression::parse("0x00"); }
colorExpr = Expression::parse("0x00");
}
ElementType getType() const override { return ElementType::Divider; } ElementType getType() const override { return ElementType::Divider; }
void setColorExpr(const std::string &expr) { void setColorExpr(const std::string& expr) {
colorExpr = Expression::parse(expr); colorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
@ -418,9 +403,8 @@ public:
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
std::string colStr = context.evaluatestring(colorExpr); std::string colStr = context.evaluatestring(colorExpr);
uint8_t color = Color::parse(colStr).value; uint8_t color = Color::parse(colStr).value;
@ -445,27 +429,26 @@ class BatteryIcon : public UIElement {
Expression valueExpr; Expression valueExpr;
Expression colorExpr; Expression colorExpr;
public: public:
BatteryIcon(const std::string &id) : UIElement(id) { BatteryIcon(const std::string& id) : UIElement(id) {
valueExpr = Expression::parse("0"); valueExpr = Expression::parse("0");
colorExpr = Expression::parse("0x00"); // Black by default colorExpr = Expression::parse("0x00"); // Black by default
} }
ElementType getType() const override { return ElementType::BatteryIcon; } ElementType getType() const override { return ElementType::BatteryIcon; }
void setValue(const std::string &expr) { void setValue(const std::string& expr) {
valueExpr = Expression::parse(expr); valueExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setColor(const std::string &expr) { void setColor(const std::string& expr) {
colorExpr = Expression::parse(expr); colorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
std::string valStr = context.evaluatestring(valueExpr); std::string valStr = context.evaluatestring(valueExpr);
int percentage = valStr.empty() ? 0 : std::stoi(valStr); int percentage = valStr.empty() ? 0 : std::stoi(valStr);
@ -480,22 +463,17 @@ public:
int x = absX; int x = absX;
int y = absY; int y = absY;
if (absW > batteryWidth) if (absW > batteryWidth) x += (absW - batteryWidth) / 2;
x += (absW - batteryWidth) / 2; if (absH > batteryHeight) y += (absH - batteryHeight) / 2;
if (absH > batteryHeight)
y += (absH - batteryHeight) / 2;
renderer.drawLine(x + 1, y, x + batteryWidth - 3, y, black); renderer.drawLine(x + 1, y, x + batteryWidth - 3, y, black);
renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1, black);
y + batteryHeight - 1, black);
renderer.drawLine(x, y + 1, x, y + batteryHeight - 2, black); renderer.drawLine(x, y + 1, x, y + batteryHeight - 2, black);
renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2, black);
y + batteryHeight - 2, black);
renderer.drawPixel(x + batteryWidth - 1, y + 3, black); renderer.drawPixel(x + batteryWidth - 1, y + 3, black);
renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4, black); renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4, black);
renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5, black);
y + batteryHeight - 5, black);
if (percentage > 0) { if (percentage > 0) {
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1; int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
@ -509,4 +487,4 @@ public:
} }
}; };
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -6,8 +6,8 @@
namespace ThemeEngine { namespace ThemeEngine {
// Use static function for C++14 ODR compatibility // Use static function for C++14 ODR compatibility
static const char *getDefaultThemeIni() { static const char* getDefaultThemeIni() {
static const char *theme = R"INI( static const char* theme = R"INI(
; ============================================ ; ============================================
; DEFAULT THEME - Original CrossPoint Reader ; DEFAULT THEME - Original CrossPoint Reader
; ============================================ ; ============================================
@ -275,4 +275,4 @@ Align = center
return theme; return theme;
} }
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -24,17 +24,15 @@ struct IniSection {
}; };
class IniParser { class IniParser {
public: public:
// Parse a stream (File, Serial, etc.) // Parse a stream (File, Serial, etc.)
static std::map<std::string, std::map<std::string, std::string>> static std::map<std::string, std::map<std::string, std::string>> parse(Stream& stream);
parse(Stream &stream);
// Parse a string buffer (useful for testing) // Parse a string buffer (useful for testing)
static std::map<std::string, std::map<std::string, std::string>> static std::map<std::string, std::map<std::string, std::string>> parseString(const std::string& content);
parseString(const std::string &content);
private: private:
static void trim(std::string &s); static void trim(std::string& s);
}; };
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -1,22 +1,23 @@
#pragma once #pragma once
#include <vector>
#include "BasicElements.h" #include "BasicElements.h"
#include "ThemeContext.h" #include "ThemeContext.h"
#include "ThemeTypes.h" #include "ThemeTypes.h"
#include "UIElement.h" #include "UIElement.h"
#include <vector>
namespace ThemeEngine { namespace ThemeEngine {
// --- HStack: Horizontal Stack Layout --- // --- HStack: Horizontal Stack Layout ---
// Children are arranged horizontally with optional spacing // Children are arranged horizontally with optional spacing
class HStack : public Container { class HStack : public Container {
int spacing = 0; // Gap between children int spacing = 0; // Gap between children
int padding = 0; // Internal padding int padding = 0; // Internal padding
bool centerVertical = false; bool centerVertical = false;
public: public:
HStack(const std::string &id) : Container(id) {} HStack(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::HStack; } ElementType getType() const override { return ElementType::HStack; }
void setSpacing(int s) { void setSpacing(int s) {
@ -32,8 +33,7 @@ public:
markDirty(); markDirty();
} }
void layout(const ThemeContext &context, int parentX, int parentY, void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
int parentW, int parentH) override {
UIElement::layout(context, parentX, parentY, parentW, parentH); UIElement::layout(context, parentX, parentY, parentW, parentH);
int currentX = absX + padding; int currentX = absX + padding;
@ -68,8 +68,8 @@ class VStack : public Container {
int padding = 0; int padding = 0;
bool centerHorizontal = false; bool centerHorizontal = false;
public: public:
VStack(const std::string &id) : Container(id) {} VStack(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::VStack; } ElementType getType() const override { return ElementType::VStack; }
void setSpacing(int s) { void setSpacing(int s) {
@ -85,8 +85,7 @@ public:
markDirty(); markDirty();
} }
void layout(const ThemeContext &context, int parentX, int parentY, void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
int parentW, int parentH) override {
UIElement::layout(context, parentX, parentY, parentW, parentH); UIElement::layout(context, parentX, parentY, parentW, parentH);
int currentY = absY + padding; int currentY = absY + padding;
@ -120,8 +119,8 @@ class Grid : public Container {
int colSpacing = 10; int colSpacing = 10;
int padding = 0; int padding = 0;
public: public:
Grid(const std::string &id) : Container(id) {} Grid(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::Grid; } ElementType getType() const override { return ElementType::Grid; }
void setColumns(int c) { void setColumns(int c) {
@ -141,12 +140,10 @@ public:
markDirty(); markDirty();
} }
void layout(const ThemeContext &context, int parentX, int parentY, void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
int parentW, int parentH) override {
UIElement::layout(context, parentX, parentY, parentW, parentH); UIElement::layout(context, parentX, parentY, parentW, parentH);
if (children.empty()) if (children.empty()) return;
return;
int availableW = absW - 2 * padding - (columns - 1) * colSpacing; int availableW = absW - 2 * padding - (columns - 1) * colSpacing;
int cellW = availableW / columns; int cellW = availableW / columns;
@ -162,8 +159,7 @@ public:
// Pass cell dimensions to avoid clamping issues // Pass cell dimensions to avoid clamping issues
child->layout(context, cellX, currentY, cellW, availableH); child->layout(context, cellX, currentY, cellW, availableH);
int childH = child->getAbsH(); int childH = child->getAbsH();
if (childH > maxRowHeight) if (childH > maxRowHeight) maxRowHeight = childH;
maxRowHeight = childH;
col++; col++;
if (col >= columns) { if (col >= columns) {
@ -184,27 +180,27 @@ class Badge : public UIElement {
Expression bgColorExpr; Expression bgColorExpr;
Expression fgColorExpr; Expression fgColorExpr;
int fontId = 0; int fontId = 0;
int paddingH = 8; // Horizontal padding int paddingH = 8; // Horizontal padding
int paddingV = 4; // Vertical padding int paddingV = 4; // Vertical padding
int cornerRadius = 0; int cornerRadius = 0;
public: public:
Badge(const std::string &id) : UIElement(id) { Badge(const std::string& id) : UIElement(id) {
bgColorExpr = Expression::parse("0x00"); // Black background bgColorExpr = Expression::parse("0x00"); // Black background
fgColorExpr = Expression::parse("0xFF"); // White text fgColorExpr = Expression::parse("0xFF"); // White text
} }
ElementType getType() const override { return ElementType::Badge; } ElementType getType() const override { return ElementType::Badge; }
void setText(const std::string &expr) { void setText(const std::string& expr) {
textExpr = Expression::parse(expr); textExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setBgColor(const std::string &expr) { void setBgColor(const std::string& expr) {
bgColorExpr = Expression::parse(expr); bgColorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setFgColor(const std::string &expr) { void setFgColor(const std::string& expr) {
fgColorExpr = Expression::parse(expr); fgColorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
@ -221,9 +217,8 @@ public:
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
std::string text = context.evaluatestring(textExpr); std::string text = context.evaluatestring(textExpr);
if (text.empty()) { if (text.empty()) {
@ -238,10 +233,8 @@ public:
int badgeH = textH + 2 * paddingV; int badgeH = textH + 2 * paddingV;
// Use absX, absY as position, but we may auto-size // Use absX, absY as position, but we may auto-size
if (absW == 0) if (absW == 0) absW = badgeW;
absW = badgeW; if (absH == 0) absH = badgeH;
if (absH == 0)
absH = badgeH;
// Draw background // Draw background
std::string bgStr = context.evaluatestring(bgColorExpr); std::string bgStr = context.evaluatestring(bgColorExpr);
@ -264,31 +257,31 @@ public:
// --- Toggle: On/Off Switch --- // --- Toggle: On/Off Switch ---
class Toggle : public UIElement { class Toggle : public UIElement {
Expression valueExpr; // Boolean expression Expression valueExpr; // Boolean expression
Expression onColorExpr; Expression onColorExpr;
Expression offColorExpr; Expression offColorExpr;
int trackWidth = 44; int trackWidth = 44;
int trackHeight = 24; int trackHeight = 24;
int knobSize = 20; int knobSize = 20;
public: public:
Toggle(const std::string &id) : UIElement(id) { Toggle(const std::string& id) : UIElement(id) {
valueExpr = Expression::parse("false"); valueExpr = Expression::parse("false");
onColorExpr = Expression::parse("0x00"); // Black when on onColorExpr = Expression::parse("0x00"); // Black when on
offColorExpr = Expression::parse("0xFF"); // White when off offColorExpr = Expression::parse("0xFF"); // White when off
} }
ElementType getType() const override { return ElementType::Toggle; } ElementType getType() const override { return ElementType::Toggle; }
void setValue(const std::string &expr) { void setValue(const std::string& expr) {
valueExpr = Expression::parse(expr); valueExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setOnColor(const std::string &expr) { void setOnColor(const std::string& expr) {
onColorExpr = Expression::parse(expr); onColorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setOffColor(const std::string &expr) { void setOffColor(const std::string& expr) {
offColorExpr = Expression::parse(expr); offColorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
@ -305,15 +298,13 @@ public:
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
bool isOn = context.evaluateBool(valueExpr.rawExpr); bool isOn = context.evaluateBool(valueExpr.rawExpr);
// Get colors // Get colors
std::string colorStr = std::string colorStr = isOn ? context.evaluatestring(onColorExpr) : context.evaluatestring(offColorExpr);
isOn ? context.evaluatestring(onColorExpr) : context.evaluatestring(offColorExpr);
uint8_t trackColor = Color::parse(colorStr).value; uint8_t trackColor = Color::parse(colorStr).value;
// Draw track // Draw track
@ -324,8 +315,7 @@ public:
// Draw knob // Draw knob
int knobMargin = (trackHeight - knobSize) / 2; int knobMargin = (trackHeight - knobSize) / 2;
int knobX = isOn ? (trackX + trackWidth - knobSize - knobMargin) int knobX = isOn ? (trackX + trackWidth - knobSize - knobMargin) : (trackX + knobMargin);
: (trackX + knobMargin);
int knobY = trackY + knobMargin; int knobY = trackY + knobMargin;
// Knob is opposite color of track // Knob is opposite color of track
@ -338,17 +328,17 @@ public:
// --- TabBar: Horizontal tab selection --- // --- TabBar: Horizontal tab selection ---
class TabBar : public Container { class TabBar : public Container {
Expression selectedExpr; // Currently selected tab index or name Expression selectedExpr; // Currently selected tab index or name
int tabSpacing = 0; int tabSpacing = 0;
int padding = 0; int padding = 0;
int indicatorHeight = 3; int indicatorHeight = 3;
bool showIndicator = true; bool showIndicator = true;
public: public:
TabBar(const std::string &id) : Container(id) {} TabBar(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::TabBar; } ElementType getType() const override { return ElementType::TabBar; }
void setSelected(const std::string &expr) { void setSelected(const std::string& expr) {
selectedExpr = Expression::parse(expr); selectedExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
@ -369,12 +359,10 @@ public:
markDirty(); markDirty();
} }
void layout(const ThemeContext &context, int parentX, int parentY, void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
int parentW, int parentH) override {
UIElement::layout(context, parentX, parentY, parentW, parentH); UIElement::layout(context, parentX, parentY, parentW, parentH);
if (children.empty()) if (children.empty()) return;
return;
// Distribute tabs evenly // Distribute tabs evenly
int numTabs = children.size(); int numTabs = children.size();
@ -389,9 +377,8 @@ public:
} }
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
// Draw background if set // Draw background if set
if (hasBg) { if (hasBg) {
@ -419,7 +406,7 @@ public:
} }
if (selectedIdx >= 0 && selectedIdx < static_cast<int>(children.size())) { if (selectedIdx >= 0 && selectedIdx < static_cast<int>(children.size())) {
UIElement *tab = children[selectedIdx]; UIElement* tab = children[selectedIdx];
int indX = tab->getAbsX(); int indX = tab->getAbsX();
int indY = absY + absH - indicatorHeight; int indY = absY + absH - indicatorHeight;
int indW = tab->getAbsW(); int indW = tab->getAbsW();
@ -437,25 +424,25 @@ public:
// --- Icon: Small symbolic image --- // --- Icon: Small symbolic image ---
// Can be a built-in icon name or a path to a BMP // Can be a built-in icon name or a path to a BMP
class Icon : public UIElement { class Icon : public UIElement {
Expression srcExpr; // Icon name or path Expression srcExpr; // Icon name or path
Expression colorExpr; Expression colorExpr;
int iconSize = 24; int iconSize = 24;
// Built-in icon names and their simple representations // Built-in icon names and their simple representations
// In a real implementation, these would be actual bitmap data // In a real implementation, these would be actual bitmap data
public: public:
Icon(const std::string &id) : UIElement(id) { Icon(const std::string& id) : UIElement(id) {
colorExpr = Expression::parse("0x00"); // Black by default colorExpr = Expression::parse("0x00"); // Black by default
} }
ElementType getType() const override { return ElementType::Icon; } ElementType getType() const override { return ElementType::Icon; }
void setSrc(const std::string &expr) { void setSrc(const std::string& expr) {
srcExpr = Expression::parse(expr); srcExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setColorExpr(const std::string &expr) { void setColorExpr(const std::string& expr) {
colorExpr = Expression::parse(expr); colorExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
@ -464,18 +451,18 @@ public:
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override; void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
}; };
// --- ScrollIndicator: Visual scroll position --- // --- ScrollIndicator: Visual scroll position ---
class ScrollIndicator : public UIElement { class ScrollIndicator : public UIElement {
Expression positionExpr; // 0.0 to 1.0 Expression positionExpr; // 0.0 to 1.0
Expression totalExpr; // Total items Expression totalExpr; // Total items
Expression visibleExpr; // Visible items Expression visibleExpr; // Visible items
int trackWidth = 4; int trackWidth = 4;
public: public:
ScrollIndicator(const std::string &id) : UIElement(id) { ScrollIndicator(const std::string& id) : UIElement(id) {
positionExpr = Expression::parse("0"); positionExpr = Expression::parse("0");
totalExpr = Expression::parse("1"); totalExpr = Expression::parse("1");
visibleExpr = Expression::parse("1"); visibleExpr = Expression::parse("1");
@ -483,15 +470,15 @@ public:
ElementType getType() const override { return ElementType::ScrollIndicator; } ElementType getType() const override { return ElementType::ScrollIndicator; }
void setPosition(const std::string &expr) { void setPosition(const std::string& expr) {
positionExpr = Expression::parse(expr); positionExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setTotal(const std::string &expr) { void setTotal(const std::string& expr) {
totalExpr = Expression::parse(expr); totalExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
void setVisibleCount(const std::string &expr) { void setVisibleCount(const std::string& expr) {
visibleExpr = Expression::parse(expr); visibleExpr = Expression::parse(expr);
markDirty(); markDirty();
} }
@ -500,9 +487,8 @@ public:
markDirty(); markDirty();
} }
void draw(const GfxRenderer &renderer, const ThemeContext &context) override { void draw(const GfxRenderer& renderer, const ThemeContext& context) override {
if (!isVisible(context)) if (!isVisible(context)) return;
return;
// Get values // Get values
std::string posStr = context.evaluatestring(positionExpr); std::string posStr = context.evaluatestring(positionExpr);
@ -526,8 +512,7 @@ public:
// Calculate thumb size and position // Calculate thumb size and position
float ratio = static_cast<float>(visible) / static_cast<float>(total); float ratio = static_cast<float>(visible) / static_cast<float>(total);
int thumbH = static_cast<int>(absH * ratio); int thumbH = static_cast<int>(absH * ratio);
if (thumbH < 20) if (thumbH < 20) thumbH = 20; // Minimum thumb size
thumbH = 20; // Minimum thumb size
int maxScroll = total - visible; int maxScroll = total - visible;
float scrollRatio = maxScroll > 0 ? position / maxScroll : 0; float scrollRatio = maxScroll > 0 ? position / maxScroll : 0;
@ -540,4 +525,4 @@ public:
} }
}; };
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -1,57 +1,58 @@
#pragma once #pragma once
#include <map>
#include <vector>
#include "BasicElements.h" #include "BasicElements.h"
#include "UIElement.h" #include "UIElement.h"
#include <map>
#include <vector>
namespace ThemeEngine { namespace ThemeEngine {
// --- List --- // --- List ---
// Supports vertical, horizontal, and grid layouts // Supports vertical, horizontal, and grid layouts
class List : public Container { class List : public Container {
public: public:
enum class Direction { Vertical, Horizontal }; enum class Direction { Vertical, Horizontal };
enum class LayoutMode { List, Grid }; enum class LayoutMode { List, Grid };
private: private:
std::string source; // Data source name (e.g., "MainMenu", "FileList") std::string source; // Data source name (e.g., "MainMenu", "FileList")
std::string itemTemplateId; // ID of the template element std::string itemTemplateId; // ID of the template element
int itemWidth = 0; // Explicit item width (0 = auto) int itemWidth = 0; // Explicit item width (0 = auto)
int itemHeight = 0; // Explicit item height (0 = auto from template) int itemHeight = 0; // Explicit item height (0 = auto from template)
int scrollOffset = 0; // Scroll position for long lists int scrollOffset = 0; // Scroll position for long lists
int visibleItems = -1; // Max visible items (-1 = auto) int visibleItems = -1; // Max visible items (-1 = auto)
int spacing = 0; // Gap between items int spacing = 0; // Gap between items
int columns = 1; // Number of columns (for grid mode) int columns = 1; // Number of columns (for grid mode)
Direction direction = Direction::Vertical; Direction direction = Direction::Vertical;
LayoutMode layoutMode = LayoutMode::List; LayoutMode layoutMode = LayoutMode::List;
// Template element reference (resolved after loading) // Template element reference (resolved after loading)
UIElement *itemTemplate = nullptr; UIElement* itemTemplate = nullptr;
public: public:
List(const std::string &id) : Container(id) {} List(const std::string& id) : Container(id) {}
ElementType getType() const override { return ElementType::List; } ElementType getType() const override { return ElementType::List; }
void setSource(const std::string &s) { void setSource(const std::string& s) {
source = s; source = s;
markDirty(); markDirty();
} }
const std::string &getSource() const { return source; } const std::string& getSource() const { return source; }
void setItemTemplateId(const std::string &id) { void setItemTemplateId(const std::string& id) {
itemTemplateId = id; itemTemplateId = id;
markDirty(); markDirty();
} }
void setItemTemplate(UIElement *elem) { void setItemTemplate(UIElement* elem) {
itemTemplate = elem; itemTemplate = elem;
markDirty(); markDirty();
} }
UIElement *getItemTemplate() const { return itemTemplate; } UIElement* getItemTemplate() const { return itemTemplate; }
void setItemWidth(int w) { void setItemWidth(int w) {
itemWidth = w; itemWidth = w;
@ -64,18 +65,14 @@ public:
} }
int getItemHeight() const { int getItemHeight() const {
if (itemHeight > 0) if (itemHeight > 0) return itemHeight;
return itemHeight; if (itemTemplate) return itemTemplate->getAbsH() > 0 ? itemTemplate->getAbsH() : 45;
if (itemTemplate)
return itemTemplate->getAbsH() > 0 ? itemTemplate->getAbsH() : 45;
return 45; return 45;
} }
int getItemWidth() const { int getItemWidth() const {
if (itemWidth > 0) if (itemWidth > 0) return itemWidth;
return itemWidth; if (itemTemplate) return itemTemplate->getAbsW() > 0 ? itemTemplate->getAbsW() : 100;
if (itemTemplate)
return itemTemplate->getAbsW() > 0 ? itemTemplate->getAbsW() : 100;
return 100; return 100;
} }
@ -98,8 +95,7 @@ public:
void setColumns(int c) { void setColumns(int c) {
columns = c > 0 ? c : 1; columns = c > 0 ? c : 1;
if (columns > 1) if (columns > 1) layoutMode = LayoutMode::Grid;
layoutMode = LayoutMode::Grid;
markDirty(); markDirty();
} }
@ -108,7 +104,7 @@ public:
markDirty(); markDirty();
} }
void setDirectionFromString(const std::string &dir) { void setDirectionFromString(const std::string& dir) {
if (dir == "Horizontal" || dir == "horizontal" || dir == "row") { if (dir == "Horizontal" || dir == "horizontal" || dir == "row") {
direction = Direction::Horizontal; direction = Direction::Horizontal;
} else { } else {
@ -123,14 +119,13 @@ public:
} }
// Resolve template reference from element map // Resolve template reference from element map
void resolveTemplate(const std::map<std::string, UIElement *> &elements) { void resolveTemplate(const std::map<std::string, UIElement*>& elements) {
if (elements.count(itemTemplateId)) { if (elements.count(itemTemplateId)) {
itemTemplate = elements.at(itemTemplateId); itemTemplate = elements.at(itemTemplateId);
} }
} }
void layout(const ThemeContext &context, int parentX, int parentY, void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) override {
int parentW, int parentH) override {
// Layout self first (bounds) // Layout self first (bounds)
UIElement::layout(context, parentX, parentY, parentW, parentH); UIElement::layout(context, parentX, parentY, parentW, parentH);
@ -142,7 +137,7 @@ public:
} }
// Draw is implemented in BasicElements.cpp // Draw is implemented in BasicElements.cpp
void draw(const GfxRenderer &renderer, const ThemeContext &context) override; void draw(const GfxRenderer& renderer, const ThemeContext& context) override;
}; };
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -11,22 +11,21 @@ namespace ThemeEngine {
struct ExpressionToken { struct ExpressionToken {
enum Type { LITERAL, VARIABLE }; enum Type { LITERAL, VARIABLE };
Type type; Type type;
std::string value; // Literal text or variable name std::string value; // Literal text or variable name
}; };
// Pre-parsed expression for efficient repeated evaluation // Pre-parsed expression for efficient repeated evaluation
struct Expression { struct Expression {
std::vector<ExpressionToken> tokens; std::vector<ExpressionToken> tokens;
std::string rawExpr; // Original expression string for complex evaluation std::string rawExpr; // Original expression string for complex evaluation
bool empty() const { return tokens.empty() && rawExpr.empty(); } bool empty() const { return tokens.empty() && rawExpr.empty(); }
static Expression parse(const std::string &str) { static Expression parse(const std::string& str) {
Expression expr; Expression expr;
expr.rawExpr = str; expr.rawExpr = str;
if (str.empty()) if (str.empty()) return expr;
return expr;
size_t start = 0; size_t start = 0;
while (start < str.length()) { while (start < str.length()) {
@ -39,8 +38,7 @@ struct Expression {
if (open > start) { if (open > start) {
// Literal before variable // Literal before variable
expr.tokens.push_back( expr.tokens.push_back({ExpressionToken::LITERAL, str.substr(start, open - start)});
{ExpressionToken::LITERAL, str.substr(start, open - start)});
} }
size_t close = str.find('}', open); size_t close = str.find('}', open);
@ -51,8 +49,7 @@ struct Expression {
} }
// Variable // Variable
expr.tokens.push_back( expr.tokens.push_back({ExpressionToken::VARIABLE, str.substr(open + 1, close - open - 1)});
{ExpressionToken::VARIABLE, str.substr(open + 1, close - open - 1)});
start = close + 1; start = close + 1;
} }
return expr; return expr;
@ -60,115 +57,94 @@ struct Expression {
}; };
class ThemeContext { class ThemeContext {
private: private:
std::map<std::string, std::string> strings; std::map<std::string, std::string> strings;
std::map<std::string, int> ints; std::map<std::string, int> ints;
std::map<std::string, bool> bools; std::map<std::string, bool> bools;
const ThemeContext *parent = nullptr; const ThemeContext* parent = nullptr;
// Helper to trim whitespace // Helper to trim whitespace
static std::string trim(const std::string &s) { static std::string trim(const std::string& s) {
size_t start = s.find_first_not_of(" \t\n\r"); size_t start = s.find_first_not_of(" \t\n\r");
if (start == std::string::npos) if (start == std::string::npos) return "";
return "";
size_t end = s.find_last_not_of(" \t\n\r"); size_t end = s.find_last_not_of(" \t\n\r");
return s.substr(start, end - start + 1); return s.substr(start, end - start + 1);
} }
// Helper to check if string is a number // Helper to check if string is a number
static bool isNumber(const std::string &s) { static bool isNumber(const std::string& s) {
if (s.empty()) if (s.empty()) return false;
return false;
size_t start = (s[0] == '-') ? 1 : 0; size_t start = (s[0] == '-') ? 1 : 0;
for (size_t i = start; i < s.length(); i++) { for (size_t i = start; i < s.length(); i++) {
if (!isdigit(s[i])) if (!isdigit(s[i])) return false;
return false;
} }
return start < s.length(); return start < s.length();
} }
public: public:
ThemeContext(const ThemeContext *parent = nullptr) : parent(parent) {} ThemeContext(const ThemeContext* parent = nullptr) : parent(parent) {}
void setString(const std::string &key, const std::string &value) { void setString(const std::string& key, const std::string& value) { strings[key] = value; }
strings[key] = value; void setInt(const std::string& key, int value) { ints[key] = value; }
} void setBool(const std::string& key, bool value) { bools[key] = value; }
void setInt(const std::string &key, int value) { ints[key] = value; }
void setBool(const std::string &key, bool value) { bools[key] = value; }
std::string getString(const std::string &key, std::string getString(const std::string& key, const std::string& defaultValue = "") const {
const std::string &defaultValue = "") const {
auto it = strings.find(key); auto it = strings.find(key);
if (it != strings.end()) if (it != strings.end()) return it->second;
return it->second; if (parent) return parent->getString(key, defaultValue);
if (parent)
return parent->getString(key, defaultValue);
return defaultValue; return defaultValue;
} }
int getInt(const std::string &key, int defaultValue = 0) const { int getInt(const std::string& key, int defaultValue = 0) const {
auto it = ints.find(key); auto it = ints.find(key);
if (it != ints.end()) if (it != ints.end()) return it->second;
return it->second; if (parent) return parent->getInt(key, defaultValue);
if (parent)
return parent->getInt(key, defaultValue);
return defaultValue; return defaultValue;
} }
bool getBool(const std::string &key, bool defaultValue = false) const { bool getBool(const std::string& key, bool defaultValue = false) const {
auto it = bools.find(key); auto it = bools.find(key);
if (it != bools.end()) if (it != bools.end()) return it->second;
return it->second; if (parent) return parent->getBool(key, defaultValue);
if (parent)
return parent->getBool(key, defaultValue);
return defaultValue; return defaultValue;
} }
bool hasKey(const std::string &key) const { bool hasKey(const std::string& key) const {
if (strings.count(key) || ints.count(key) || bools.count(key)) if (strings.count(key) || ints.count(key) || bools.count(key)) return true;
return true; if (parent) return parent->hasKey(key);
if (parent)
return parent->hasKey(key);
return false; return false;
} }
// Get any value as string // Get any value as string
std::string getAnyAsString(const std::string &key) const { std::string getAnyAsString(const std::string& key) const {
// Check strings first // Check strings first
auto sit = strings.find(key); auto sit = strings.find(key);
if (sit != strings.end()) if (sit != strings.end()) return sit->second;
return sit->second;
// Check ints // Check ints
auto iit = ints.find(key); auto iit = ints.find(key);
if (iit != ints.end()) if (iit != ints.end()) return std::to_string(iit->second);
return std::to_string(iit->second);
// Check bools // Check bools
auto bit = bools.find(key); auto bit = bools.find(key);
if (bit != bools.end()) if (bit != bools.end()) return bit->second ? "true" : "false";
return bit->second ? "true" : "false";
// Check parent // Check parent
if (parent) if (parent) return parent->getAnyAsString(key);
return parent->getAnyAsString(key);
return ""; return "";
} }
// Evaluate a complex boolean expression // Evaluate a complex boolean expression
// Supports: !, &&, ||, ==, !=, <, >, <=, >=, parentheses // Supports: !, &&, ||, ==, !=, <, >, <=, >=, parentheses
bool evaluateBool(const std::string &expression) const { bool evaluateBool(const std::string& expression) const {
std::string expr = trim(expression); std::string expr = trim(expression);
if (expr.empty()) if (expr.empty()) return false;
return false;
// Handle literal true/false // Handle literal true/false
if (expr == "true" || expr == "1") if (expr == "true" || expr == "1") return true;
return true; if (expr == "false" || expr == "0") return false;
if (expr == "false" || expr == "0")
return false;
// Handle {var} wrapper // Handle {var} wrapper
if (expr.size() > 2 && expr.front() == '{' && expr.back() == '}') { if (expr.size() > 2 && expr.front() == '{' && expr.back() == '}') {
@ -185,10 +161,8 @@ public:
int depth = 1; int depth = 1;
size_t closePos = 1; size_t closePos = 1;
while (closePos < expr.length() && depth > 0) { while (closePos < expr.length() && depth > 0) {
if (expr[closePos] == '(') if (expr[closePos] == '(') depth++;
depth++; if (expr[closePos] == ')') depth--;
if (expr[closePos] == ')')
depth--;
closePos++; closePos++;
} }
if (closePos <= expr.length()) { if (closePos <= expr.length()) {
@ -212,14 +186,11 @@ public:
size_t orPos = expr.find("||"); size_t orPos = expr.find("||");
// Process || first (lower precedence than &&) // Process || first (lower precedence than &&)
if (orPos != std::string::npos && if (orPos != std::string::npos && (andPos == std::string::npos || orPos < andPos)) {
(andPos == std::string::npos || orPos < andPos)) { return evaluateBool(expr.substr(0, orPos)) || evaluateBool(expr.substr(orPos + 2));
return evaluateBool(expr.substr(0, orPos)) ||
evaluateBool(expr.substr(orPos + 2));
} }
if (andPos != std::string::npos) { if (andPos != std::string::npos) {
return evaluateBool(expr.substr(0, andPos)) && return evaluateBool(expr.substr(0, andPos)) && evaluateBool(expr.substr(andPos + 2));
evaluateBool(expr.substr(andPos + 2));
} }
// Handle comparisons // Handle comparisons
@ -270,7 +241,7 @@ public:
} }
// Compare two values (handles variables, numbers, strings) // Compare two values (handles variables, numbers, strings)
int compareValues(const std::string &left, const std::string &right) const { int compareValues(const std::string& left, const std::string& right) const {
std::string leftVal = resolveValue(left); std::string leftVal = resolveValue(left);
std::string rightVal = resolveValue(right); std::string rightVal = resolveValue(right);
@ -286,7 +257,7 @@ public:
} }
// Resolve a value (variable name -> value, or literal) // Resolve a value (variable name -> value, or literal)
std::string resolveValue(const std::string &val) const { std::string resolveValue(const std::string& val) const {
std::string v = trim(val); std::string v = trim(val);
// Remove quotes for string literals // Remove quotes for string literals
@ -298,8 +269,7 @@ public:
} }
// If it's a number, return as-is // If it's a number, return as-is
if (isNumber(v)) if (isNumber(v)) return v;
return v;
// Check for hex color literals (0x00, 0xFF, etc.) // Check for hex color literals (0x00, 0xFF, etc.)
if (v.size() > 2 && v[0] == '0' && (v[1] == 'x' || v[1] == 'X')) { if (v.size() > 2 && v[0] == '0' && (v[1] == 'x' || v[1] == 'X')) {
@ -326,12 +296,11 @@ public:
} }
// Evaluate a string expression with variable substitution // Evaluate a string expression with variable substitution
std::string evaluatestring(const Expression &expr) const { std::string evaluatestring(const Expression& expr) const {
if (expr.empty()) if (expr.empty()) return "";
return "";
std::string result; std::string result;
for (const auto &token : expr.tokens) { for (const auto& token : expr.tokens) {
if (token.type == ExpressionToken::LITERAL) { if (token.type == ExpressionToken::LITERAL) {
result += token.value; result += token.value;
} else { } else {
@ -339,10 +308,8 @@ public:
std::string varName = token.value; std::string varName = token.value;
// If the variable contains comparison operators, evaluate as condition // If the variable contains comparison operators, evaluate as condition
if (varName.find("==") != std::string::npos || if (varName.find("==") != std::string::npos || varName.find("!=") != std::string::npos ||
varName.find("!=") != std::string::npos || varName.find("&&") != std::string::npos || varName.find("||") != std::string::npos) {
varName.find("&&") != std::string::npos ||
varName.find("||") != std::string::npos) {
result += evaluateBool(varName) ? "true" : "false"; result += evaluateBool(varName) ? "true" : "false";
continue; continue;
} }
@ -355,7 +322,7 @@ public:
std::string condition = trim(varName.substr(0, qPos)); std::string condition = trim(varName.substr(0, qPos));
std::string trueVal = trim(varName.substr(qPos + 1, cPos - qPos - 1)); std::string trueVal = trim(varName.substr(qPos + 1, cPos - qPos - 1));
std::string falseVal = trim(varName.substr(cPos + 1)); std::string falseVal = trim(varName.substr(cPos + 1));
bool condResult = evaluateBool(condition); bool condResult = evaluateBool(condition);
result += resolveValue(condResult ? trueVal : falseVal); result += resolveValue(condResult ? trueVal : falseVal);
continue; continue;
@ -371,12 +338,11 @@ public:
} }
// Legacy method for backward compatibility // Legacy method for backward compatibility
std::string evaluateString(const std::string &expression) const { std::string evaluateString(const std::string& expression) const {
if (expression.empty()) if (expression.empty()) return "";
return "";
Expression expr = Expression::parse(expression); Expression expr = Expression::parse(expression);
return evaluatestring(expr); return evaluatestring(expr);
} }
}; };
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -1,12 +1,14 @@
#pragma once #pragma once
#include <GfxRenderer.h>
#include <map>
#include <string>
#include <vector>
#include "BasicElements.h" #include "BasicElements.h"
#include "IniParser.h" #include "IniParser.h"
#include "ThemeContext.h" #include "ThemeContext.h"
#include <GfxRenderer.h>
#include <map>
#include <string>
#include <vector>
namespace ThemeEngine { namespace ThemeEngine {
@ -18,10 +20,10 @@ struct ProcessedAsset {
// Screen render cache - stores full screen state for quick restore // Screen render cache - stores full screen state for quick restore
struct ScreenCache { struct ScreenCache {
uint8_t *buffer = nullptr; uint8_t* buffer = nullptr;
size_t bufferSize = 0; size_t bufferSize = 0;
std::string screenName; std::string screenName;
uint32_t contextHash = 0; // Hash of context data to detect changes uint32_t contextHash = 0; // Hash of context data to detect changes
bool valid = false; bool valid = false;
~ScreenCache() { ~ScreenCache() {
@ -35,8 +37,8 @@ struct ScreenCache {
}; };
class ThemeManager { class ThemeManager {
private: private:
std::map<std::string, UIElement *> elements; // All elements by ID std::map<std::string, UIElement*> elements; // All elements by ID
std::string currentThemeName; std::string currentThemeName;
int navBookCount = 1; // Number of navigable book slots (from theme [Global] section) int navBookCount = 1; // Number of navigable book slots (from theme [Global] section)
std::map<std::string, int> fontMap; std::map<std::string, int> fontMap;
@ -49,12 +51,11 @@ private:
std::map<std::string, bool> elementDependsOnData; std::map<std::string, bool> elementDependsOnData;
// Factory and property methods // Factory and property methods
UIElement *createElement(const std::string &id, const std::string &type); UIElement* createElement(const std::string& id, const std::string& type);
void applyProperties(UIElement *elem, void applyProperties(UIElement* elem, const std::map<std::string, std::string>& props);
const std::map<std::string, std::string> &props);
public: public:
static ThemeManager &get() { static ThemeManager& get() {
static ThemeManager instance; static ThemeManager instance;
return instance; return instance;
} }
@ -63,66 +64,59 @@ public:
void begin(); void begin();
// Register a font ID mapping (e.g. "UI_12" -> 0) // Register a font ID mapping (e.g. "UI_12" -> 0)
void registerFont(const std::string &name, int id); void registerFont(const std::string& name, int id);
// Theme loading // Theme loading
void loadTheme(const std::string &themeName); void loadTheme(const std::string& themeName);
void unloadTheme(); void unloadTheme();
// Get current theme name // Get current theme name
const std::string &getCurrentTheme() const { return currentThemeName; } const std::string& getCurrentTheme() const { return currentThemeName; }
// Get number of navigable book slots (from theme config, default 1) // Get number of navigable book slots (from theme config, default 1)
int getNavBookCount() const { return navBookCount; } int getNavBookCount() const { return navBookCount; }
// Render a screen // Render a screen
void renderScreen(const std::string &screenName, const GfxRenderer &renderer, void renderScreen(const std::string& screenName, const GfxRenderer& renderer, const ThemeContext& context);
const ThemeContext &context);
// Render with dirty tracking (only redraws changed regions) // Render with dirty tracking (only redraws changed regions)
void renderScreenOptimized(const std::string &screenName, void renderScreenOptimized(const std::string& screenName, const GfxRenderer& renderer, const ThemeContext& context,
const GfxRenderer &renderer, const ThemeContext* prevContext = nullptr);
const ThemeContext &context,
const ThemeContext *prevContext = nullptr);
// Invalidate all caches (call when theme changes or screen switches) // Invalidate all caches (call when theme changes or screen switches)
void invalidateAllCaches(); void invalidateAllCaches();
// Invalidate specific screen cache // Invalidate specific screen cache
void invalidateScreenCache(const std::string &screenName); void invalidateScreenCache(const std::string& screenName);
// Enable/disable caching // Enable/disable caching
void setCachingEnabled(bool enabled) { useCaching = enabled; } void setCachingEnabled(bool enabled) { useCaching = enabled; }
bool isCachingEnabled() const { return useCaching; } bool isCachingEnabled() const { return useCaching; }
// Asset path resolution // Asset path resolution
std::string getAssetPath(const std::string &assetName); std::string getAssetPath(const std::string& assetName);
// Asset caching // Asset caching
const std::vector<uint8_t> *getCachedAsset(const std::string &path); const std::vector<uint8_t>* getCachedAsset(const std::string& path);
const ProcessedAsset *getProcessedAsset(const std::string &path, const ProcessedAsset* getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
GfxRenderer::Orientation orientation,
int targetW = 0, int targetH = 0); int targetW = 0, int targetH = 0);
void cacheProcessedAsset(const std::string &path, void cacheProcessedAsset(const std::string& path, const ProcessedAsset& asset, int targetW = 0, int targetH = 0);
const ProcessedAsset &asset,
int targetW = 0, int targetH = 0);
// Clear asset caches (for memory management) // Clear asset caches (for memory management)
void clearAssetCaches(); void clearAssetCaches();
// Get element by ID (useful for direct manipulation) // Get element by ID (useful for direct manipulation)
UIElement *getElement(const std::string &id) { UIElement* getElement(const std::string& id) {
auto it = elements.find(id); auto it = elements.find(id);
return it != elements.end() ? it->second : nullptr; return it != elements.end() ? it->second : nullptr;
} }
private: private:
std::map<std::string, std::vector<uint8_t>> assetCache; std::map<std::string, std::vector<uint8_t>> assetCache;
std::map<std::string, ProcessedAsset> processedCache; std::map<std::string, ProcessedAsset> processedCache;
// Compute a simple hash of context data for cache invalidation // Compute a simple hash of context data for cache invalidation
uint32_t computeContextHash(const ThemeContext &context, uint32_t computeContextHash(const ThemeContext& context, const std::string& screenName);
const std::string &screenName);
}; };
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -14,13 +14,11 @@ struct Dimension {
Dimension(int v, DimensionUnit u) : value(v), unit(u) {} Dimension(int v, DimensionUnit u) : value(v), unit(u) {}
Dimension() : value(0), unit(DimensionUnit::PIXELS) {} Dimension() : value(0), unit(DimensionUnit::PIXELS) {}
static Dimension parse(const std::string &str) { static Dimension parse(const std::string& str) {
if (str.empty()) if (str.empty()) return Dimension(0, DimensionUnit::PIXELS);
return Dimension(0, DimensionUnit::PIXELS);
if (str.back() == '%') { if (str.back() == '%') {
return Dimension(std::stoi(str.substr(0, str.length() - 1)), return Dimension(std::stoi(str.substr(0, str.length() - 1)), DimensionUnit::PERCENT);
DimensionUnit::PERCENT);
} }
return Dimension(std::stoi(str), DimensionUnit::PIXELS); return Dimension(std::stoi(str), DimensionUnit::PIXELS);
} }
@ -34,20 +32,16 @@ struct Dimension {
}; };
struct Color { struct Color {
uint8_t value; // For E-Ink: 0 (Black) to 255 (White), or simplified palette uint8_t value; // For E-Ink: 0 (Black) to 255 (White), or simplified palette
Color(uint8_t v) : value(v) {} Color(uint8_t v) : value(v) {}
Color() : value(0) {} Color() : value(0) {}
static Color parse(const std::string &str) { static Color parse(const std::string& str) {
if (str.empty()) if (str.empty()) return Color(0);
return Color(0); if (str == "black") return Color(0x00);
if (str == "black") if (str == "white") return Color(0xFF);
return Color(0x00); if (str == "gray" || str == "grey") return Color(0x80);
if (str == "white")
return Color(0xFF);
if (str == "gray" || str == "grey")
return Color(0x80);
if (str.size() > 2 && str.substr(0, 2) == "0x") { if (str.size() > 2 && str.substr(0, 2) == "0x") {
return Color((uint8_t)std::strtol(str.c_str(), nullptr, 16)); return Color((uint8_t)std::strtol(str.c_str(), nullptr, 16));
} }
@ -64,16 +58,13 @@ struct Rect {
bool isEmpty() const { return w <= 0 || h <= 0; } bool isEmpty() const { return w <= 0 || h <= 0; }
bool intersects(const Rect &other) const { bool intersects(const Rect& other) const {
return !(x + w <= other.x || other.x + other.w <= x || y + h <= other.y || return !(x + w <= other.x || other.x + other.w <= x || y + h <= other.y || other.y + other.h <= y);
other.y + other.h <= y);
} }
Rect unite(const Rect &other) const { Rect unite(const Rect& other) const {
if (isEmpty()) if (isEmpty()) return other;
return other; if (other.isEmpty()) return *this;
if (other.isEmpty())
return *this;
int nx = std::min(x, other.x); int nx = std::min(x, other.x);
int ny = std::min(y, other.y); int ny = std::min(y, other.y);
int nx2 = std::max(x + w, other.x + other.w); int nx2 = std::max(x + w, other.x + other.w);
@ -82,4 +73,4 @@ struct Rect {
} }
}; };
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -1,52 +1,51 @@
#pragma once #pragma once
#include "ThemeContext.h"
#include "ThemeTypes.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <string> #include <string>
#include <vector> #include <vector>
#include "ThemeContext.h"
#include "ThemeTypes.h"
namespace ThemeEngine { namespace ThemeEngine {
class Container; // Forward declaration class Container; // Forward declaration
class UIElement { class UIElement {
public: public:
int getAbsX() const { return absX; } int getAbsX() const { return absX; }
int getAbsY() const { return absY; } int getAbsY() const { return absY; }
int getAbsW() const { return absW; } int getAbsW() const { return absW; }
int getAbsH() const { return absH; } int getAbsH() const { return absH; }
const std::string &getId() const { return id; } const std::string& getId() const { return id; }
protected: protected:
std::string id; std::string id;
Dimension x, y, width, height; Dimension x, y, width, height;
Expression visibleExpr; Expression visibleExpr;
bool visibleExprIsStatic = true; // True if visibility doesn't depend on data bool visibleExprIsStatic = true; // True if visibility doesn't depend on data
// Recomputed every layout pass // Recomputed every layout pass
int absX = 0, absY = 0, absW = 0, absH = 0; int absX = 0, absY = 0, absW = 0, absH = 0;
// Caching support // Caching support
bool cacheable = false; // Set true for expensive elements like bitmaps bool cacheable = false; // Set true for expensive elements like bitmaps
bool cacheValid = false; // Is the cached render still valid? bool cacheValid = false; // Is the cached render still valid?
uint8_t *cachedRender = nullptr; uint8_t* cachedRender = nullptr;
size_t cachedRenderSize = 0; size_t cachedRenderSize = 0;
int cachedX = 0, cachedY = 0, cachedW = 0, cachedH = 0; int cachedX = 0, cachedY = 0, cachedW = 0, cachedH = 0;
// Dirty tracking // Dirty tracking
bool dirty = true; // Needs redraw bool dirty = true; // Needs redraw
bool isVisible(const ThemeContext &context) const { bool isVisible(const ThemeContext& context) const {
if (visibleExpr.empty()) if (visibleExpr.empty()) return true;
return true;
return context.evaluateBool(visibleExpr.rawExpr); return context.evaluateBool(visibleExpr.rawExpr);
} }
public: public:
UIElement(const std::string &id) : id(id) { UIElement(const std::string& id) : id(id) { visibleExpr = Expression::parse("true"); }
visibleExpr = Expression::parse("true");
}
virtual ~UIElement() { virtual ~UIElement() {
if (cachedRender) { if (cachedRender) {
@ -71,11 +70,11 @@ public:
height = val; height = val;
markDirty(); markDirty();
} }
void setVisibleExpr(const std::string &expr) { void setVisibleExpr(const std::string& expr) {
visibleExpr = Expression::parse(expr); visibleExpr = Expression::parse(expr);
// Check if expression contains variables // Check if expression contains variables
visibleExprIsStatic = (expr == "true" || expr == "false" || expr == "1" || visibleExprIsStatic =
expr == "0" || expr.find('{') == std::string::npos); (expr == "true" || expr == "false" || expr == "1" || expr == "0" || expr.find('{') == std::string::npos);
markDirty(); markDirty();
} }
@ -97,31 +96,24 @@ public:
} }
// Calculate absolute position based on parent // Calculate absolute position based on parent
virtual void layout(const ThemeContext &context, int parentX, int parentY, virtual void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) {
int parentW, int parentH) {
int newX = parentX + x.resolve(parentW); int newX = parentX + x.resolve(parentW);
int newY = parentY + y.resolve(parentH); int newY = parentY + y.resolve(parentH);
int newW = width.resolve(parentW); int newW = width.resolve(parentW);
int newH = height.resolve(parentH); int newH = height.resolve(parentH);
// Clamp to parent bounds // Clamp to parent bounds
if (newX >= parentX + parentW) if (newX >= parentX + parentW) newX = parentX + parentW - 1;
newX = parentX + parentW - 1; if (newY >= parentY + parentH) newY = parentY + parentH - 1;
if (newY >= parentY + parentH)
newY = parentY + parentH - 1;
int maxX = parentX + parentW; int maxX = parentX + parentW;
int maxY = parentY + parentH; int maxY = parentY + parentH;
if (newX + newW > maxX) if (newX + newW > maxX) newW = maxX - newX;
newW = maxX - newX; if (newY + newH > maxY) newH = maxY - newY;
if (newY + newH > maxY)
newH = maxY - newY;
if (newW < 0) if (newW < 0) newW = 0;
newW = 0; if (newH < 0) newH = 0;
if (newH < 0)
newH = 0;
// Check if position changed // Check if position changed
if (newX != absX || newY != absY || newW != absW || newH != absH) { if (newX != absX || newY != absY || newW != absW || newH != absH) {
@ -133,7 +125,7 @@ public:
} }
} }
virtual Container *asContainer() { return nullptr; } virtual Container* asContainer() { return nullptr; }
enum class ElementType { enum class ElementType {
Base, Base,
@ -166,12 +158,11 @@ public:
Rect getBounds() const { return Rect(absX, absY, absW, absH); } Rect getBounds() const { return Rect(absX, absY, absW, absH); }
// Main draw method - handles caching automatically // Main draw method - handles caching automatically
virtual void draw(const GfxRenderer &renderer, virtual void draw(const GfxRenderer& renderer, const ThemeContext& context) = 0;
const ThemeContext &context) = 0;
protected: protected:
// Cache the rendered output // Cache the rendered output
bool cacheRender(const GfxRenderer &renderer) { bool cacheRender(const GfxRenderer& renderer) {
if (cachedRender) { if (cachedRender) {
free(cachedRender); free(cachedRender);
cachedRender = nullptr; cachedRender = nullptr;
@ -190,16 +181,13 @@ protected:
} }
// Restore from cache // Restore from cache
bool restoreFromCache(const GfxRenderer &renderer) const { bool restoreFromCache(const GfxRenderer& renderer) const {
if (!cacheValid || !cachedRender) if (!cacheValid || !cachedRender) return false;
return false; if (absX != cachedX || absY != cachedY || absW != cachedW || absH != cachedH) return false;
if (absX != cachedX || absY != cachedY || absW != cachedW ||
absH != cachedH)
return false;
renderer.restoreRegion(cachedRender, absX, absY, absW, absH); renderer.restoreRegion(cachedRender, absX, absY, absW, absH);
return true; return true;
} }
}; };
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -1,4 +1,5 @@
#include "BasicElements.h" #include "BasicElements.h"
#include "Bitmap.h" #include "Bitmap.h"
#include "ListElement.h" #include "ListElement.h"
#include "ThemeManager.h" #include "ThemeManager.h"
@ -7,8 +8,7 @@
namespace ThemeEngine { namespace ThemeEngine {
// --- BitmapElement --- // --- BitmapElement ---
void BitmapElement::draw(const GfxRenderer &renderer, void BitmapElement::draw(const GfxRenderer& renderer, const ThemeContext& context) {
const ThemeContext &context) {
if (!isVisible(context)) { if (!isVisible(context)) {
markClean(); markClean();
return; return;
@ -21,13 +21,12 @@ void BitmapElement::draw(const GfxRenderer &renderer,
} }
// Check if we have a cached 1-bit render of this bitmap at this size // Check if we have a cached 1-bit render of this bitmap at this size
const ProcessedAsset *processed = const ProcessedAsset* processed = ThemeManager::get().getProcessedAsset(path, renderer.getOrientation(), absW, absH);
ThemeManager::get().getProcessedAsset(path, renderer.getOrientation(), absW, absH);
if (processed && processed->w == absW && processed->h == absH) { if (processed && processed->w == absW && processed->h == absH) {
// Draw cached 1-bit data directly // Draw cached 1-bit data directly
const int rowBytes = (absW + 7) / 8; const int rowBytes = (absW + 7) / 8;
for (int y = 0; y < absH; y++) { for (int y = 0; y < absH; y++) {
const uint8_t *srcRow = processed->data.data() + y * rowBytes; const uint8_t* srcRow = processed->data.data() + y * rowBytes;
for (int x = 0; x < absW; x++) { for (int x = 0; x < absW; x++) {
bool isBlack = !(srcRow[x / 8] & (1 << (7 - (x % 8)))); bool isBlack = !(srcRow[x / 8] & (1 << (7 - (x % 8))));
if (isBlack) { if (isBlack) {
@ -40,7 +39,7 @@ void BitmapElement::draw(const GfxRenderer &renderer,
} }
// Load raw asset from cache (file data cached in memory) // Load raw asset from cache (file data cached in memory)
const std::vector<uint8_t> *data = ThemeManager::get().getCachedAsset(path); const std::vector<uint8_t>* data = ThemeManager::get().getCachedAsset(path);
if (!data || data->empty()) { if (!data || data->empty()) {
markClean(); markClean();
return; return;
@ -54,19 +53,19 @@ void BitmapElement::draw(const GfxRenderer &renderer,
// Draw the bitmap (handles scaling internally) // Draw the bitmap (handles scaling internally)
renderer.drawBitmap(bmp, absX, absY, absW, absH); renderer.drawBitmap(bmp, absX, absY, absW, absH);
// Cache the result as 1-bit packed data for next time // Cache the result as 1-bit packed data for next time
ProcessedAsset asset; ProcessedAsset asset;
asset.w = absW; asset.w = absW;
asset.h = absH; asset.h = absH;
asset.orientation = renderer.getOrientation(); asset.orientation = renderer.getOrientation();
const int rowBytes = (absW + 7) / 8; const int rowBytes = (absW + 7) / 8;
asset.data.resize(rowBytes * absH, 0xFF); // Initialize to white asset.data.resize(rowBytes * absH, 0xFF); // Initialize to white
// Capture pixels using renderer's coordinate system // Capture pixels using renderer's coordinate system
for (int y = 0; y < absH; y++) { for (int y = 0; y < absH; y++) {
uint8_t *dstRow = asset.data.data() + y * rowBytes; uint8_t* dstRow = asset.data.data() + y * rowBytes;
for (int x = 0; x < absW; x++) { for (int x = 0; x < absW; x++) {
// Read pixel from framebuffer (this handles orientation) // Read pixel from framebuffer (this handles orientation)
bool isBlack = renderer.readPixel(absX + x, absY + y); bool isBlack = renderer.readPixel(absX + x, absY + y);
@ -75,14 +74,14 @@ void BitmapElement::draw(const GfxRenderer &renderer,
} }
} }
} }
ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH); ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH);
markClean(); markClean();
} }
// --- List --- // --- List ---
void List::draw(const GfxRenderer &renderer, const ThemeContext &context) { void List::draw(const GfxRenderer& renderer, const ThemeContext& context) {
if (!isVisible(context)) { if (!isVisible(context)) {
markClean(); markClean();
return; return;
@ -148,8 +147,7 @@ void List::draw(const GfxRenderer &renderer, const ThemeContext &context) {
currentX += itemW + spacing; currentX += itemW + spacing;
continue; continue;
} }
if (currentX > absX + absW) if (currentX > absX + absW) break;
break;
} else { } else {
// Grid mode // Grid mode
if (currentY + itemH < absY) { if (currentY + itemH < absY) {
@ -162,8 +160,7 @@ void List::draw(const GfxRenderer &renderer, const ThemeContext &context) {
currentX = absX + col * (itemW + spacing); currentX = absX + col * (itemW + spacing);
continue; continue;
} }
if (currentY > absY + absH) if (currentY > absY + absH) break;
break;
} }
// Layout and draw // Layout and draw
@ -225,4 +222,4 @@ void List::draw(const GfxRenderer &renderer, const ThemeContext &context) {
markClean(); markClean();
} }
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -1,11 +1,11 @@
#include "IniParser.h" #include "IniParser.h"
#include <sstream> #include <sstream>
namespace ThemeEngine { namespace ThemeEngine {
void IniParser::trim(std::string &s) { void IniParser::trim(std::string& s) {
if (s.empty()) if (s.empty()) return;
return;
// Trim left // Trim left
size_t first = s.find_first_not_of(" \t\n\r"); size_t first = s.find_first_not_of(" \t\n\r");
@ -19,14 +19,13 @@ void IniParser::trim(std::string &s) {
s = s.substr(first, (last - first + 1)); s = s.substr(first, (last - first + 1));
} }
std::map<std::string, std::map<std::string, std::string>> std::map<std::string, std::map<std::string, std::string>> IniParser::parse(Stream& stream) {
IniParser::parse(Stream &stream) {
std::map<std::string, std::map<std::string, std::string>> sections; std::map<std::string, std::map<std::string, std::string>> sections;
// stream check not strictly possible like file, can rely on available() // stream check not strictly possible like file, can rely on available()
std::string currentSection = ""; std::string currentSection = "";
String line; // Use Arduino String for easy file reading, then convert to String line; // Use Arduino String for easy file reading, then convert to
// std::string // std::string
while (stream.available()) { while (stream.available()) {
line = stream.readStringUntil('\n'); line = stream.readStringUntil('\n');
@ -34,7 +33,7 @@ IniParser::parse(Stream &stream) {
trim(sLine); trim(sLine);
if (sLine.empty() || sLine[0] == ';' || sLine[0] == '#') { if (sLine.empty() || sLine[0] == ';' || sLine[0] == '#') {
continue; // Skip comments and empty lines continue; // Skip comments and empty lines
} }
if (sLine.front() == '[' && sLine.back() == ']') { if (sLine.front() == '[' && sLine.back() == ']') {
@ -62,8 +61,7 @@ IniParser::parse(Stream &stream) {
return sections; return sections;
} }
std::map<std::string, std::map<std::string, std::string>> std::map<std::string, std::map<std::string, std::string>> IniParser::parseString(const std::string& content) {
IniParser::parseString(const std::string &content) {
std::map<std::string, std::map<std::string, std::string>> sections; std::map<std::string, std::map<std::string, std::string>> sections;
std::stringstream ss(content); std::stringstream ss(content);
std::string line; std::string line;
@ -101,4 +99,4 @@ IniParser::parseString(const std::string &content) {
return sections; return sections;
} }
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -1,12 +1,14 @@
#include "LayoutElements.h" #include "LayoutElements.h"
#include "ThemeManager.h"
#include <Bitmap.h> #include <Bitmap.h>
#include "ThemeManager.h"
namespace ThemeEngine { namespace ThemeEngine {
// Built-in icon drawing // Built-in icon drawing
// These are simple geometric representations of common icons // These are simple geometric representations of common icons
void Icon::draw(const GfxRenderer &renderer, const ThemeContext &context) { void Icon::draw(const GfxRenderer& renderer, const ThemeContext& context) {
if (!isVisible(context)) { if (!isVisible(context)) {
markClean(); markClean();
return; return;
@ -29,15 +31,14 @@ void Icon::draw(const GfxRenderer &renderer, const ThemeContext &context) {
int cy = absY + h / 2; int cy = absY + h / 2;
// Check if it's a path to a BMP file // Check if it's a path to a BMP file
if (iconName.find('/') != std::string::npos || if (iconName.find('/') != std::string::npos || iconName.find('.') != std::string::npos) {
iconName.find('.') != std::string::npos) {
// Try to load as bitmap // Try to load as bitmap
std::string path = iconName; std::string path = iconName;
if (path[0] != '/') { if (path[0] != '/') {
path = ThemeManager::get().getAssetPath(iconName); path = ThemeManager::get().getAssetPath(iconName);
} }
const std::vector<uint8_t> *data = ThemeManager::get().getCachedAsset(path); const std::vector<uint8_t>* data = ThemeManager::get().getCachedAsset(path);
if (data && !data->empty()) { if (data && !data->empty()) {
Bitmap bmp(data->data(), data->size()); Bitmap bmp(data->data(), data->size());
if (bmp.parseHeaders() == BmpReaderError::Ok) { if (bmp.parseHeaders() == BmpReaderError::Ok) {
@ -170,4 +171,4 @@ void Icon::draw(const GfxRenderer &renderer, const ThemeContext &context) {
markClean(); markClean();
} }
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -1,82 +1,63 @@
#include "ThemeManager.h" #include "ThemeManager.h"
#include "DefaultTheme.h"
#include "LayoutElements.h"
#include "ListElement.h"
#include <SDCardManager.h> #include <SDCardManager.h>
#include <algorithm> #include <algorithm>
#include <map> #include <map>
#include <vector> #include <vector>
#include "DefaultTheme.h"
#include "LayoutElements.h"
#include "ListElement.h"
namespace ThemeEngine { namespace ThemeEngine {
void ThemeManager::begin() { void ThemeManager::begin() {
// Default fonts or setup // Default fonts or setup
} }
void ThemeManager::registerFont(const std::string &name, int id) { void ThemeManager::registerFont(const std::string& name, int id) { fontMap[name] = id; }
fontMap[name] = id;
}
std::string ThemeManager::getAssetPath(const std::string &assetName) { std::string ThemeManager::getAssetPath(const std::string& assetName) {
// Check if absolute path // Check if absolute path
if (!assetName.empty() && assetName[0] == '/') if (!assetName.empty() && assetName[0] == '/') return assetName;
return assetName;
// Otherwise relative to theme assets // Otherwise relative to theme assets
return "/themes/" + currentThemeName + "/assets/" + assetName; return "/themes/" + currentThemeName + "/assets/" + assetName;
} }
UIElement *ThemeManager::createElement(const std::string &id, UIElement* ThemeManager::createElement(const std::string& id, const std::string& type) {
const std::string &type) {
// Basic elements // Basic elements
if (type == "Container") if (type == "Container") return new Container(id);
return new Container(id); if (type == "Rectangle") return new Rectangle(id);
if (type == "Rectangle") if (type == "Label") return new Label(id);
return new Rectangle(id); if (type == "Bitmap") return new BitmapElement(id);
if (type == "Label") if (type == "List") return new List(id);
return new Label(id); if (type == "ProgressBar") return new ProgressBar(id);
if (type == "Bitmap") if (type == "Divider") return new Divider(id);
return new BitmapElement(id);
if (type == "List")
return new List(id);
if (type == "ProgressBar")
return new ProgressBar(id);
if (type == "Divider")
return new Divider(id);
// Layout elements // Layout elements
if (type == "HStack") if (type == "HStack") return new HStack(id);
return new HStack(id); if (type == "VStack") return new VStack(id);
if (type == "VStack") if (type == "Grid") return new Grid(id);
return new VStack(id);
if (type == "Grid")
return new Grid(id);
// Advanced elements // Advanced elements
if (type == "Badge") if (type == "Badge") return new Badge(id);
return new Badge(id); if (type == "Toggle") return new Toggle(id);
if (type == "Toggle") if (type == "TabBar") return new TabBar(id);
return new Toggle(id); if (type == "Icon") return new Icon(id);
if (type == "TabBar") if (type == "ScrollIndicator") return new ScrollIndicator(id);
return new TabBar(id); if (type == "BatteryIcon") return new BatteryIcon(id);
if (type == "Icon")
return new Icon(id);
if (type == "ScrollIndicator")
return new ScrollIndicator(id);
if (type == "BatteryIcon")
return new BatteryIcon(id);
return nullptr; return nullptr;
} }
void ThemeManager::applyProperties( void ThemeManager::applyProperties(UIElement* elem, const std::map<std::string, std::string>& props) {
UIElement *elem, const std::map<std::string, std::string> &props) {
const auto elemType = elem->getType(); const auto elemType = elem->getType();
for (const auto &kv : props) { for (const auto& kv : props) {
const std::string &key = kv.first; const std::string& key = kv.first;
const std::string &val = kv.second; const std::string& val = kv.second;
// ========== Common properties ========== // ========== Common properties ==========
if (key == "X") if (key == "X")
@ -95,7 +76,7 @@ void ThemeManager::applyProperties(
// ========== Rectangle properties ========== // ========== Rectangle properties ==========
else if (key == "Fill") { else if (key == "Fill") {
if (elemType == UIElement::ElementType::Rectangle) { if (elemType == UIElement::ElementType::Rectangle) {
auto rect = static_cast<Rectangle *>(elem); auto rect = static_cast<Rectangle*>(elem);
if (val.find('{') != std::string::npos) { if (val.find('{') != std::string::npos) {
rect->setFillExpr(val); rect->setFillExpr(val);
} else { } else {
@ -104,21 +85,19 @@ void ThemeManager::applyProperties(
} }
} else if (key == "Color") { } else if (key == "Color") {
if (elemType == UIElement::ElementType::Rectangle) { if (elemType == UIElement::ElementType::Rectangle) {
static_cast<Rectangle *>(elem)->setColorExpr(val); static_cast<Rectangle*>(elem)->setColorExpr(val);
} else if (elemType == UIElement::ElementType::Container || } else if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::HStack || elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid ||
elemType == UIElement::ElementType::VStack ||
elemType == UIElement::ElementType::Grid ||
elemType == UIElement::ElementType::TabBar) { elemType == UIElement::ElementType::TabBar) {
static_cast<Container *>(elem)->setBackgroundColorExpr(val); static_cast<Container*>(elem)->setBackgroundColorExpr(val);
} else if (elemType == UIElement::ElementType::Label) { } else if (elemType == UIElement::ElementType::Label) {
static_cast<Label *>(elem)->setColorExpr(val); static_cast<Label*>(elem)->setColorExpr(val);
} else if (elemType == UIElement::ElementType::Divider) { } else if (elemType == UIElement::ElementType::Divider) {
static_cast<Divider *>(elem)->setColorExpr(val); static_cast<Divider*>(elem)->setColorExpr(val);
} else if (elemType == UIElement::ElementType::Icon) { } else if (elemType == UIElement::ElementType::Icon) {
static_cast<Icon *>(elem)->setColorExpr(val); static_cast<Icon*>(elem)->setColorExpr(val);
} else if (elemType == UIElement::ElementType::BatteryIcon) { } else if (elemType == UIElement::ElementType::BatteryIcon) {
static_cast<BatteryIcon *>(elem)->setColor(val); static_cast<BatteryIcon*>(elem)->setColor(val);
} }
} }
@ -132,278 +111,264 @@ void ThemeManager::applyProperties(
} }
} }
} else if (key == "Padding") { } else if (key == "Padding") {
if (elemType == UIElement::ElementType::Container || if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::HStack || elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid) {
elemType == UIElement::ElementType::VStack || static_cast<Container*>(elem)->setPadding(std::stoi(val));
elemType == UIElement::ElementType::Grid) {
static_cast<Container *>(elem)->setPadding(std::stoi(val));
} else if (elemType == UIElement::ElementType::TabBar) { } else if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar *>(elem)->setPadding(std::stoi(val)); static_cast<TabBar*>(elem)->setPadding(std::stoi(val));
} }
} else if (key == "BorderRadius") { } else if (key == "BorderRadius") {
if (elemType == UIElement::ElementType::Container || if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::HStack || elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid) {
elemType == UIElement::ElementType::VStack || static_cast<Container*>(elem)->setBorderRadius(std::stoi(val));
elemType == UIElement::ElementType::Grid) {
static_cast<Container *>(elem)->setBorderRadius(std::stoi(val));
} }
} else if (key == "Spacing") { } else if (key == "Spacing") {
if (elemType == UIElement::ElementType::HStack) { if (elemType == UIElement::ElementType::HStack) {
static_cast<HStack *>(elem)->setSpacing(std::stoi(val)); static_cast<HStack*>(elem)->setSpacing(std::stoi(val));
} else if (elemType == UIElement::ElementType::VStack) { } else if (elemType == UIElement::ElementType::VStack) {
static_cast<VStack *>(elem)->setSpacing(std::stoi(val)); static_cast<VStack*>(elem)->setSpacing(std::stoi(val));
} else if (elemType == UIElement::ElementType::List) { } else if (elemType == UIElement::ElementType::List) {
static_cast<List *>(elem)->setSpacing(std::stoi(val)); static_cast<List*>(elem)->setSpacing(std::stoi(val));
} }
} else if (key == "CenterVertical") { } else if (key == "CenterVertical") {
if (elemType == UIElement::ElementType::HStack) { if (elemType == UIElement::ElementType::HStack) {
static_cast<HStack *>(elem)->setCenterVertical(val == "true" || val == "1"); static_cast<HStack*>(elem)->setCenterVertical(val == "true" || val == "1");
} }
} else if (key == "CenterHorizontal") { } else if (key == "CenterHorizontal") {
if (elemType == UIElement::ElementType::VStack) { if (elemType == UIElement::ElementType::VStack) {
static_cast<VStack *>(elem)->setCenterHorizontal(val == "true" || val == "1"); static_cast<VStack*>(elem)->setCenterHorizontal(val == "true" || val == "1");
} }
} }
// ========== Label properties ========== // ========== Label properties ==========
else if (key == "Text") { else if (key == "Text") {
if (elemType == UIElement::ElementType::Label) { if (elemType == UIElement::ElementType::Label) {
static_cast<Label *>(elem)->setText(val); static_cast<Label*>(elem)->setText(val);
} else if (elemType == UIElement::ElementType::Badge) { } else if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge *>(elem)->setText(val); static_cast<Badge*>(elem)->setText(val);
} }
} else if (key == "Font") { } else if (key == "Font") {
if (elemType == UIElement::ElementType::Label) { if (elemType == UIElement::ElementType::Label) {
if (fontMap.count(val)) { if (fontMap.count(val)) {
static_cast<Label *>(elem)->setFont(fontMap[val]); static_cast<Label*>(elem)->setFont(fontMap[val]);
} }
} else if (elemType == UIElement::ElementType::Badge) { } else if (elemType == UIElement::ElementType::Badge) {
if (fontMap.count(val)) { if (fontMap.count(val)) {
static_cast<Badge *>(elem)->setFont(fontMap[val]); static_cast<Badge*>(elem)->setFont(fontMap[val]);
} }
} }
} else if (key == "Centered") { } else if (key == "Centered") {
if (elemType == UIElement::ElementType::Label) { if (elemType == UIElement::ElementType::Label) {
static_cast<Label *>(elem)->setCentered(val == "true" || val == "1"); static_cast<Label*>(elem)->setCentered(val == "true" || val == "1");
} }
} else if (key == "Align") { } else if (key == "Align") {
if (elemType == UIElement::ElementType::Label) { if (elemType == UIElement::ElementType::Label) {
Label::Alignment align = Label::Alignment::Left; Label::Alignment align = Label::Alignment::Left;
if (val == "Center" || val == "center") if (val == "Center" || val == "center") align = Label::Alignment::Center;
align = Label::Alignment::Center; if (val == "Right" || val == "right") align = Label::Alignment::Right;
if (val == "Right" || val == "right") static_cast<Label*>(elem)->setAlignment(align);
align = Label::Alignment::Right;
static_cast<Label *>(elem)->setAlignment(align);
} }
} else if (key == "MaxLines") { } else if (key == "MaxLines") {
if (elemType == UIElement::ElementType::Label) { if (elemType == UIElement::ElementType::Label) {
static_cast<Label *>(elem)->setMaxLines(std::stoi(val)); static_cast<Label*>(elem)->setMaxLines(std::stoi(val));
} }
} else if (key == "Ellipsis") { } else if (key == "Ellipsis") {
if (elemType == UIElement::ElementType::Label) { if (elemType == UIElement::ElementType::Label) {
static_cast<Label *>(elem)->setEllipsis(val == "true" || val == "1"); static_cast<Label*>(elem)->setEllipsis(val == "true" || val == "1");
} }
} }
// ========== Bitmap/Icon properties ========== // ========== Bitmap/Icon properties ==========
else if (key == "Src") { else if (key == "Src") {
if (elemType == UIElement::ElementType::Bitmap) { if (elemType == UIElement::ElementType::Bitmap) {
auto b = static_cast<BitmapElement *>(elem); auto b = static_cast<BitmapElement*>(elem);
if (val.find('{') == std::string::npos && if (val.find('{') == std::string::npos && val.find('/') == std::string::npos) {
val.find('/') == std::string::npos) {
b->setSrc(getAssetPath(val)); b->setSrc(getAssetPath(val));
} else { } else {
b->setSrc(val); b->setSrc(val);
} }
} else if (elemType == UIElement::ElementType::Icon) { } else if (elemType == UIElement::ElementType::Icon) {
static_cast<Icon *>(elem)->setSrc(val); static_cast<Icon*>(elem)->setSrc(val);
} }
} else if (key == "ScaleToFit") { } else if (key == "ScaleToFit") {
if (elemType == UIElement::ElementType::Bitmap) { if (elemType == UIElement::ElementType::Bitmap) {
static_cast<BitmapElement *>(elem)->setScaleToFit(val == "true" || static_cast<BitmapElement*>(elem)->setScaleToFit(val == "true" || val == "1");
val == "1");
} }
} else if (key == "PreserveAspect") { } else if (key == "PreserveAspect") {
if (elemType == UIElement::ElementType::Bitmap) { if (elemType == UIElement::ElementType::Bitmap) {
static_cast<BitmapElement *>(elem)->setPreserveAspect(val == "true" || static_cast<BitmapElement*>(elem)->setPreserveAspect(val == "true" || val == "1");
val == "1");
} }
} else if (key == "IconSize") { } else if (key == "IconSize") {
if (elemType == UIElement::ElementType::Icon) { if (elemType == UIElement::ElementType::Icon) {
static_cast<Icon *>(elem)->setIconSize(std::stoi(val)); static_cast<Icon*>(elem)->setIconSize(std::stoi(val));
} }
} }
// ========== List properties ========== // ========== List properties ==========
else if (key == "Source") { else if (key == "Source") {
if (elemType == UIElement::ElementType::List) { if (elemType == UIElement::ElementType::List) {
static_cast<List *>(elem)->setSource(val); static_cast<List*>(elem)->setSource(val);
} }
} else if (key == "ItemTemplate") { } else if (key == "ItemTemplate") {
if (elemType == UIElement::ElementType::List) { if (elemType == UIElement::ElementType::List) {
static_cast<List *>(elem)->setItemTemplateId(val); static_cast<List*>(elem)->setItemTemplateId(val);
} }
} else if (key == "ItemHeight") { } else if (key == "ItemHeight") {
if (elemType == UIElement::ElementType::List) { if (elemType == UIElement::ElementType::List) {
static_cast<List *>(elem)->setItemHeight(std::stoi(val)); static_cast<List*>(elem)->setItemHeight(std::stoi(val));
} }
} else if (key == "ItemWidth") { } else if (key == "ItemWidth") {
if (elemType == UIElement::ElementType::List) { if (elemType == UIElement::ElementType::List) {
static_cast<List *>(elem)->setItemWidth(std::stoi(val)); static_cast<List*>(elem)->setItemWidth(std::stoi(val));
} }
} else if (key == "Direction") { } else if (key == "Direction") {
if (elemType == UIElement::ElementType::List) { if (elemType == UIElement::ElementType::List) {
static_cast<List *>(elem)->setDirectionFromString(val); static_cast<List*>(elem)->setDirectionFromString(val);
} }
} else if (key == "Columns") { } else if (key == "Columns") {
if (elemType == UIElement::ElementType::List) { if (elemType == UIElement::ElementType::List) {
static_cast<List *>(elem)->setColumns(std::stoi(val)); static_cast<List*>(elem)->setColumns(std::stoi(val));
} else if (elemType == UIElement::ElementType::Grid) { } else if (elemType == UIElement::ElementType::Grid) {
static_cast<Grid *>(elem)->setColumns(std::stoi(val)); static_cast<Grid*>(elem)->setColumns(std::stoi(val));
} }
} else if (key == "RowSpacing") { } else if (key == "RowSpacing") {
if (elemType == UIElement::ElementType::Grid) { if (elemType == UIElement::ElementType::Grid) {
static_cast<Grid *>(elem)->setRowSpacing(std::stoi(val)); static_cast<Grid*>(elem)->setRowSpacing(std::stoi(val));
} }
} else if (key == "ColSpacing") { } else if (key == "ColSpacing") {
if (elemType == UIElement::ElementType::Grid) { if (elemType == UIElement::ElementType::Grid) {
static_cast<Grid *>(elem)->setColSpacing(std::stoi(val)); static_cast<Grid*>(elem)->setColSpacing(std::stoi(val));
} }
} }
// ========== ProgressBar properties ========== // ========== ProgressBar properties ==========
else if (key == "Value") { else if (key == "Value") {
if (elemType == UIElement::ElementType::ProgressBar) { if (elemType == UIElement::ElementType::ProgressBar) {
static_cast<ProgressBar *>(elem)->setValue(val); static_cast<ProgressBar*>(elem)->setValue(val);
} else if (elemType == UIElement::ElementType::Toggle) { } else if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle *>(elem)->setValue(val); static_cast<Toggle*>(elem)->setValue(val);
} else if (elemType == UIElement::ElementType::BatteryIcon) { } else if (elemType == UIElement::ElementType::BatteryIcon) {
static_cast<BatteryIcon *>(elem)->setValue(val); static_cast<BatteryIcon*>(elem)->setValue(val);
} }
} else if (key == "Max") { } else if (key == "Max") {
if (elemType == UIElement::ElementType::ProgressBar) { if (elemType == UIElement::ElementType::ProgressBar) {
static_cast<ProgressBar *>(elem)->setMax(val); static_cast<ProgressBar*>(elem)->setMax(val);
} }
} else if (key == "FgColor") { } else if (key == "FgColor") {
if (elemType == UIElement::ElementType::ProgressBar) { if (elemType == UIElement::ElementType::ProgressBar) {
static_cast<ProgressBar *>(elem)->setFgColor(val); static_cast<ProgressBar*>(elem)->setFgColor(val);
} else if (elemType == UIElement::ElementType::Badge) { } else if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge *>(elem)->setFgColor(val); static_cast<Badge*>(elem)->setFgColor(val);
} }
} else if (key == "BgColor") { } else if (key == "BgColor") {
if (elemType == UIElement::ElementType::ProgressBar) { if (elemType == UIElement::ElementType::ProgressBar) {
static_cast<ProgressBar *>(elem)->setBgColor(val); static_cast<ProgressBar*>(elem)->setBgColor(val);
} else if (elemType == UIElement::ElementType::Badge) { } else if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge *>(elem)->setBgColor(val); static_cast<Badge*>(elem)->setBgColor(val);
} else if (elemType == UIElement::ElementType::Container || } else if (elemType == UIElement::ElementType::Container || elemType == UIElement::ElementType::HStack ||
elemType == UIElement::ElementType::HStack || elemType == UIElement::ElementType::VStack || elemType == UIElement::ElementType::Grid) {
elemType == UIElement::ElementType::VStack || static_cast<Container*>(elem)->setBackgroundColorExpr(val);
elemType == UIElement::ElementType::Grid) {
static_cast<Container *>(elem)->setBackgroundColorExpr(val);
} }
} else if (key == "ShowBorder") { } else if (key == "ShowBorder") {
if (elemType == UIElement::ElementType::ProgressBar) { if (elemType == UIElement::ElementType::ProgressBar) {
static_cast<ProgressBar *>(elem)->setShowBorder(val == "true" || static_cast<ProgressBar*>(elem)->setShowBorder(val == "true" || val == "1");
val == "1");
} }
} }
// ========== Divider properties ========== // ========== Divider properties ==========
else if (key == "Horizontal") { else if (key == "Horizontal") {
if (elemType == UIElement::ElementType::Divider) { if (elemType == UIElement::ElementType::Divider) {
static_cast<Divider *>(elem)->setHorizontal(val == "true" || val == "1"); static_cast<Divider*>(elem)->setHorizontal(val == "true" || val == "1");
} }
} else if (key == "Thickness") { } else if (key == "Thickness") {
if (elemType == UIElement::ElementType::Divider) { if (elemType == UIElement::ElementType::Divider) {
static_cast<Divider *>(elem)->setThickness(std::stoi(val)); static_cast<Divider*>(elem)->setThickness(std::stoi(val));
} }
} }
// ========== Toggle properties ========== // ========== Toggle properties ==========
else if (key == "OnColor") { else if (key == "OnColor") {
if (elemType == UIElement::ElementType::Toggle) { if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle *>(elem)->setOnColor(val); static_cast<Toggle*>(elem)->setOnColor(val);
} }
} else if (key == "OffColor") { } else if (key == "OffColor") {
if (elemType == UIElement::ElementType::Toggle) { if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle *>(elem)->setOffColor(val); static_cast<Toggle*>(elem)->setOffColor(val);
} }
} else if (key == "TrackWidth") { } else if (key == "TrackWidth") {
if (elemType == UIElement::ElementType::Toggle) { if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle *>(elem)->setTrackWidth(std::stoi(val)); static_cast<Toggle*>(elem)->setTrackWidth(std::stoi(val));
} else if (elemType == UIElement::ElementType::ScrollIndicator) { } else if (elemType == UIElement::ElementType::ScrollIndicator) {
static_cast<ScrollIndicator *>(elem)->setTrackWidth(std::stoi(val)); static_cast<ScrollIndicator*>(elem)->setTrackWidth(std::stoi(val));
} }
} else if (key == "TrackHeight") { } else if (key == "TrackHeight") {
if (elemType == UIElement::ElementType::Toggle) { if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle *>(elem)->setTrackHeight(std::stoi(val)); static_cast<Toggle*>(elem)->setTrackHeight(std::stoi(val));
} }
} else if (key == "KnobSize") { } else if (key == "KnobSize") {
if (elemType == UIElement::ElementType::Toggle) { if (elemType == UIElement::ElementType::Toggle) {
static_cast<Toggle *>(elem)->setKnobSize(std::stoi(val)); static_cast<Toggle*>(elem)->setKnobSize(std::stoi(val));
} }
} }
// ========== TabBar properties ========== // ========== TabBar properties ==========
else if (key == "Selected") { else if (key == "Selected") {
if (elemType == UIElement::ElementType::TabBar) { if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar *>(elem)->setSelected(val); static_cast<TabBar*>(elem)->setSelected(val);
} }
} else if (key == "TabSpacing") { } else if (key == "TabSpacing") {
if (elemType == UIElement::ElementType::TabBar) { if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar *>(elem)->setTabSpacing(std::stoi(val)); static_cast<TabBar*>(elem)->setTabSpacing(std::stoi(val));
} }
} else if (key == "IndicatorHeight") { } else if (key == "IndicatorHeight") {
if (elemType == UIElement::ElementType::TabBar) { if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar *>(elem)->setIndicatorHeight(std::stoi(val)); static_cast<TabBar*>(elem)->setIndicatorHeight(std::stoi(val));
} }
} else if (key == "ShowIndicator") { } else if (key == "ShowIndicator") {
if (elemType == UIElement::ElementType::TabBar) { if (elemType == UIElement::ElementType::TabBar) {
static_cast<TabBar *>(elem)->setShowIndicator(val == "true" || val == "1"); static_cast<TabBar*>(elem)->setShowIndicator(val == "true" || val == "1");
} }
} }
// ========== ScrollIndicator properties ========== // ========== ScrollIndicator properties ==========
else if (key == "Position") { else if (key == "Position") {
if (elemType == UIElement::ElementType::ScrollIndicator) { if (elemType == UIElement::ElementType::ScrollIndicator) {
static_cast<ScrollIndicator *>(elem)->setPosition(val); static_cast<ScrollIndicator*>(elem)->setPosition(val);
} }
} else if (key == "Total") { } else if (key == "Total") {
if (elemType == UIElement::ElementType::ScrollIndicator) { if (elemType == UIElement::ElementType::ScrollIndicator) {
static_cast<ScrollIndicator *>(elem)->setTotal(val); static_cast<ScrollIndicator*>(elem)->setTotal(val);
} }
} else if (key == "VisibleCount") { } else if (key == "VisibleCount") {
if (elemType == UIElement::ElementType::ScrollIndicator) { if (elemType == UIElement::ElementType::ScrollIndicator) {
static_cast<ScrollIndicator *>(elem)->setVisibleCount(val); static_cast<ScrollIndicator*>(elem)->setVisibleCount(val);
} }
} }
// ========== Badge properties ========== // ========== Badge properties ==========
else if (key == "PaddingH") { else if (key == "PaddingH") {
if (elemType == UIElement::ElementType::Badge) { if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge *>(elem)->setPaddingH(std::stoi(val)); static_cast<Badge*>(elem)->setPaddingH(std::stoi(val));
} }
} else if (key == "PaddingV") { } else if (key == "PaddingV") {
if (elemType == UIElement::ElementType::Badge) { if (elemType == UIElement::ElementType::Badge) {
static_cast<Badge *>(elem)->setPaddingV(std::stoi(val)); static_cast<Badge*>(elem)->setPaddingV(std::stoi(val));
} }
} }
} }
} }
const std::vector<uint8_t> * const std::vector<uint8_t>* ThemeManager::getCachedAsset(const std::string& path) {
ThemeManager::getCachedAsset(const std::string &path) {
if (assetCache.count(path)) { if (assetCache.count(path)) {
return &assetCache.at(path); return &assetCache.at(path);
} }
if (!SdMan.exists(path.c_str())) if (!SdMan.exists(path.c_str())) return nullptr;
return nullptr;
FsFile file; FsFile file;
if (SdMan.openFileForRead("ThemeCache", path, file)) { if (SdMan.openFileForRead("ThemeCache", path, file)) {
size_t size = file.size(); size_t size = file.size();
auto &buf = assetCache[path]; auto& buf = assetCache[path];
buf.resize(size); buf.resize(size);
file.read(buf.data(), size); file.read(buf.data(), size);
file.close(); file.close();
@ -412,18 +377,16 @@ ThemeManager::getCachedAsset(const std::string &path) {
return nullptr; return nullptr;
} }
const ProcessedAsset * const ProcessedAsset* ThemeManager::getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation,
ThemeManager::getProcessedAsset(const std::string &path, int targetW, int targetH) {
GfxRenderer::Orientation orientation,
int targetW, int targetH) {
// Include dimensions in cache key for scaled images // Include dimensions in cache key for scaled images
std::string cacheKey = path; std::string cacheKey = path;
if (targetW > 0 && targetH > 0) { if (targetW > 0 && targetH > 0) {
cacheKey += ":" + std::to_string(targetW) + "x" + std::to_string(targetH); cacheKey += ":" + std::to_string(targetW) + "x" + std::to_string(targetH);
} }
if (processedCache.count(cacheKey)) { if (processedCache.count(cacheKey)) {
const auto &asset = processedCache.at(cacheKey); const auto& asset = processedCache.at(cacheKey);
if (asset.orientation == orientation) { if (asset.orientation == orientation) {
return &asset; return &asset;
} }
@ -431,9 +394,7 @@ ThemeManager::getProcessedAsset(const std::string &path,
return nullptr; return nullptr;
} }
void ThemeManager::cacheProcessedAsset(const std::string &path, void ThemeManager::cacheProcessedAsset(const std::string& path, const ProcessedAsset& asset, int targetW, int targetH) {
const ProcessedAsset &asset,
int targetW, int targetH) {
std::string cacheKey = path; std::string cacheKey = path;
if (targetW > 0 && targetH > 0) { if (targetW > 0 && targetH > 0) {
cacheKey += ":" + std::to_string(targetW) + "x" + std::to_string(targetH); cacheKey += ":" + std::to_string(targetW) + "x" + std::to_string(targetH);
@ -456,19 +417,18 @@ void ThemeManager::unloadTheme() {
} }
void ThemeManager::invalidateAllCaches() { void ThemeManager::invalidateAllCaches() {
for (auto &kv : screenCaches) { for (auto& kv : screenCaches) {
kv.second.invalidate(); kv.second.invalidate();
} }
} }
void ThemeManager::invalidateScreenCache(const std::string &screenName) { void ThemeManager::invalidateScreenCache(const std::string& screenName) {
if (screenCaches.count(screenName)) { if (screenCaches.count(screenName)) {
screenCaches[screenName].invalidate(); screenCaches[screenName].invalidate();
} }
} }
uint32_t ThemeManager::computeContextHash(const ThemeContext &context, uint32_t ThemeManager::computeContextHash(const ThemeContext& context, const std::string& screenName) {
const std::string &screenName) {
uint32_t hash = 2166136261u; uint32_t hash = 2166136261u;
for (char c : screenName) { for (char c : screenName) {
hash ^= static_cast<uint32_t>(c); hash ^= static_cast<uint32_t>(c);
@ -477,7 +437,7 @@ uint32_t ThemeManager::computeContextHash(const ThemeContext &context,
return hash; return hash;
} }
void ThemeManager::loadTheme(const std::string &themeName) { void ThemeManager::loadTheme(const std::string& themeName) {
unloadTheme(); unloadTheme();
currentThemeName = themeName; currentThemeName = themeName;
@ -500,7 +460,7 @@ void ThemeManager::loadTheme(const std::string &themeName) {
} else { } else {
std::string path = "/themes/" + themeName + "/theme.ini"; std::string path = "/themes/" + themeName + "/theme.ini";
Serial.printf("[ThemeManager] Checking path: %s\n", path.c_str()); Serial.printf("[ThemeManager] Checking path: %s\n", path.c_str());
if (!SdMan.exists(path.c_str())) { if (!SdMan.exists(path.c_str())) {
Serial.printf("[ThemeManager] Theme %s not found, using Default\n", themeName.c_str()); Serial.printf("[ThemeManager] Theme %s not found, using Default\n", themeName.c_str());
sections = IniParser::parseString(getDefaultThemeIni()); sections = IniParser::parseString(getDefaultThemeIni());
@ -511,8 +471,7 @@ void ThemeManager::loadTheme(const std::string &themeName) {
Serial.printf("[ThemeManager] Parsing theme file...\n"); Serial.printf("[ThemeManager] Parsing theme file...\n");
sections = IniParser::parse(file); sections = IniParser::parse(file);
file.close(); file.close();
Serial.printf("[ThemeManager] Parsed %d sections from %s\n", Serial.printf("[ThemeManager] Parsed %d sections from %s\n", (int)sections.size(), themeName.c_str());
(int)sections.size(), themeName.c_str());
} else { } else {
Serial.printf("[ThemeManager] Failed to open %s, using Default\n", path.c_str()); Serial.printf("[ThemeManager] Failed to open %s, using Default\n", path.c_str());
sections = IniParser::parseString(getDefaultThemeIni()); sections = IniParser::parseString(getDefaultThemeIni());
@ -522,63 +481,57 @@ void ThemeManager::loadTheme(const std::string &themeName) {
} }
// Read theme configuration from [Global] section // Read theme configuration from [Global] section
navBookCount = 1; // Default navBookCount = 1; // Default
if (sections.count("Global")) { if (sections.count("Global")) {
const auto &global = sections.at("Global"); const auto& global = sections.at("Global");
if (global.count("NavBookCount")) { if (global.count("NavBookCount")) {
navBookCount = std::stoi(global.at("NavBookCount")); navBookCount = std::stoi(global.at("NavBookCount"));
if (navBookCount < 1) navBookCount = 1; if (navBookCount < 1) navBookCount = 1;
if (navBookCount > 10) navBookCount = 10; // Reasonable max if (navBookCount > 10) navBookCount = 10; // Reasonable max
} }
} }
// Pass 1: Creation // Pass 1: Creation
for (const auto &sec : sections) { for (const auto& sec : sections) {
std::string id = sec.first; std::string id = sec.first;
const std::map<std::string, std::string> &props = sec.second; const std::map<std::string, std::string>& props = sec.second;
if (id == "Global") if (id == "Global") continue;
continue;
auto it = props.find("Type"); auto it = props.find("Type");
if (it == props.end()) if (it == props.end()) continue;
continue;
std::string type = it->second; std::string type = it->second;
if (type.empty()) if (type.empty()) continue;
continue;
UIElement *elem = createElement(id, type); UIElement* elem = createElement(id, type);
if (elem) { if (elem) {
elements[id] = elem; elements[id] = elem;
} }
} }
// Pass 2: Properties & Parent Wiring // Pass 2: Properties & Parent Wiring
std::vector<List *> lists; std::vector<List*> lists;
for (const auto &sec : sections) { for (const auto& sec : sections) {
std::string id = sec.first; std::string id = sec.first;
if (id == "Global") if (id == "Global") continue;
continue; if (elements.find(id) == elements.end()) continue;
if (elements.find(id) == elements.end())
continue;
UIElement *elem = elements[id]; UIElement* elem = elements[id];
applyProperties(elem, sec.second); applyProperties(elem, sec.second);
if (elem->getType() == UIElement::ElementType::List) { if (elem->getType() == UIElement::ElementType::List) {
lists.push_back(static_cast<List *>(elem)); lists.push_back(static_cast<List*>(elem));
} }
if (sec.second.count("Parent")) { if (sec.second.count("Parent")) {
std::string parentId = sec.second.at("Parent"); std::string parentId = sec.second.at("Parent");
if (elements.count(parentId)) { if (elements.count(parentId)) {
UIElement *parent = elements[parentId]; UIElement* parent = elements[parentId];
if (auto c = parent->asContainer()) { if (auto c = parent->asContainer()) {
c->addChild(elem); c->addChild(elem);
} }
} else { } else {
Serial.printf("[ThemeManager] WARN: Parent %s not found for %s\n", Serial.printf("[ThemeManager] WARN: Parent %s not found for %s\n", parentId.c_str(), id.c_str());
parentId.c_str(), id.c_str());
} }
} }
} }
@ -588,31 +541,26 @@ void ThemeManager::loadTheme(const std::string &themeName) {
l->resolveTemplate(elements); l->resolveTemplate(elements);
} }
Serial.printf("[ThemeManager] Theme loaded with %d elements\n", Serial.printf("[ThemeManager] Theme loaded with %d elements\n", (int)elements.size());
(int)elements.size());
} }
void ThemeManager::renderScreen(const std::string &screenName, void ThemeManager::renderScreen(const std::string& screenName, const GfxRenderer& renderer,
const GfxRenderer &renderer, const ThemeContext& context) {
const ThemeContext &context) {
if (elements.count(screenName) == 0) { if (elements.count(screenName) == 0) {
Serial.printf("[ThemeManager] Screen '%s' not found\n", screenName.c_str()); Serial.printf("[ThemeManager] Screen '%s' not found\n", screenName.c_str());
return; return;
} }
UIElement *root = elements[screenName]; UIElement* root = elements[screenName];
root->layout(context, 0, 0, renderer.getScreenWidth(), root->layout(context, 0, 0, renderer.getScreenWidth(), renderer.getScreenHeight());
renderer.getScreenHeight());
root->draw(renderer, context); root->draw(renderer, context);
} }
void ThemeManager::renderScreenOptimized(const std::string &screenName, void ThemeManager::renderScreenOptimized(const std::string& screenName, const GfxRenderer& renderer,
const GfxRenderer &renderer, const ThemeContext& context, const ThemeContext* prevContext) {
const ThemeContext &context,
const ThemeContext *prevContext) {
renderScreen(screenName, renderer, context); renderScreen(screenName, renderer, context);
} }
} // namespace ThemeEngine } // namespace ThemeEngine

View File

@ -16,7 +16,7 @@ constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields // Increment this when adding new persisted settings fields
constexpr uint8_t SETTINGS_COUNT = 21; constexpr uint8_t SETTINGS_COUNT = 21;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
bool CrossPointSettings::saveToFile() const { bool CrossPointSettings::saveToFile() const {
// Make sure the directory exists // Make sure the directory exists
@ -65,8 +65,7 @@ bool CrossPointSettings::loadFromFile() {
uint8_t version; uint8_t version;
serialization::readPod(inputFile, version); serialization::readPod(inputFile, version);
if (version != SETTINGS_FILE_VERSION) { if (version != SETTINGS_FILE_VERSION) {
Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", Serial.printf("[%lu] [CPS] Deserialization failed: Unknown version %u\n", millis(), version);
millis(), version);
inputFile.close(); inputFile.close();
return false; return false;
} }
@ -78,78 +77,57 @@ bool CrossPointSettings::loadFromFile() {
uint8_t settingsRead = 0; uint8_t settingsRead = 0;
do { do {
serialization::readPod(inputFile, sleepScreen); serialization::readPod(inputFile, sleepScreen);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, extraParagraphSpacing); serialization::readPod(inputFile, extraParagraphSpacing);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, shortPwrBtn); serialization::readPod(inputFile, shortPwrBtn);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, statusBar); serialization::readPod(inputFile, statusBar);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, orientation); serialization::readPod(inputFile, orientation);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, frontButtonLayout); serialization::readPod(inputFile, frontButtonLayout);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, sideButtonLayout); serialization::readPod(inputFile, sideButtonLayout);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, fontFamily); serialization::readPod(inputFile, fontFamily);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, fontSize); serialization::readPod(inputFile, fontSize);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, lineSpacing); serialization::readPod(inputFile, lineSpacing);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, paragraphAlignment); serialization::readPod(inputFile, paragraphAlignment);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, sleepTimeout); serialization::readPod(inputFile, sleepTimeout);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, refreshFrequency); serialization::readPod(inputFile, refreshFrequency);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, screenMargin); serialization::readPod(inputFile, screenMargin);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, sleepScreenCoverMode); serialization::readPod(inputFile, sleepScreenCoverMode);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
{ {
std::string urlStr; std::string urlStr;
serialization::readString(inputFile, urlStr); serialization::readString(inputFile, urlStr);
strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1); strncpy(opdsServerUrl, urlStr.c_str(), sizeof(opdsServerUrl) - 1);
opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0'; opdsServerUrl[sizeof(opdsServerUrl) - 1] = '\0';
} }
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, textAntiAliasing); serialization::readPod(inputFile, textAntiAliasing);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, hideBatteryPercentage); serialization::readPod(inputFile, hideBatteryPercentage);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, longPressChapterSkip); serialization::readPod(inputFile, longPressChapterSkip);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
serialization::readPod(inputFile, hyphenationEnabled); serialization::readPod(inputFile, hyphenationEnabled);
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
{ {
std::string themeStr; std::string themeStr;
serialization::readString(inputFile, themeStr); serialization::readString(inputFile, themeStr);
strncpy(themeName, themeStr.c_str(), sizeof(themeName) - 1); strncpy(themeName, themeStr.c_str(), sizeof(themeName) - 1);
themeName[sizeof(themeName) - 1] = '\0'; themeName[sizeof(themeName) - 1] = '\0';
} }
if (++settingsRead >= fileSettingsCount) if (++settingsRead >= fileSettingsCount) break;
break;
} while (false); } while (false);
inputFile.close(); inputFile.close();
@ -159,110 +137,110 @@ bool CrossPointSettings::loadFromFile() {
float CrossPointSettings::getReaderLineCompression() const { float CrossPointSettings::getReaderLineCompression() const {
switch (fontFamily) { switch (fontFamily) {
case BOOKERLY: case BOOKERLY:
default:
switch (lineSpacing) {
case TIGHT:
return 0.95f;
case NORMAL:
default: default:
return 1.0f; switch (lineSpacing) {
case WIDE: case TIGHT:
return 1.1f; return 0.95f;
} case NORMAL:
case NOTOSANS: default:
switch (lineSpacing) { return 1.0f;
case TIGHT: case WIDE:
return 0.90f; return 1.1f;
case NORMAL: }
default: case NOTOSANS:
return 0.95f; switch (lineSpacing) {
case WIDE: case TIGHT:
return 1.0f; return 0.90f;
} case NORMAL:
case OPENDYSLEXIC: default:
switch (lineSpacing) { return 0.95f;
case TIGHT: case WIDE:
return 0.90f; return 1.0f;
case NORMAL: }
default: case OPENDYSLEXIC:
return 0.95f; switch (lineSpacing) {
case WIDE: case TIGHT:
return 1.0f; return 0.90f;
} case NORMAL:
default:
return 0.95f;
case WIDE:
return 1.0f;
}
} }
} }
unsigned long CrossPointSettings::getSleepTimeoutMs() const { unsigned long CrossPointSettings::getSleepTimeoutMs() const {
switch (sleepTimeout) { switch (sleepTimeout) {
case SLEEP_1_MIN: case SLEEP_1_MIN:
return 1UL * 60 * 1000; return 1UL * 60 * 1000;
case SLEEP_5_MIN: case SLEEP_5_MIN:
return 5UL * 60 * 1000; return 5UL * 60 * 1000;
case SLEEP_10_MIN: case SLEEP_10_MIN:
default: default:
return 10UL * 60 * 1000; return 10UL * 60 * 1000;
case SLEEP_15_MIN: case SLEEP_15_MIN:
return 15UL * 60 * 1000; return 15UL * 60 * 1000;
case SLEEP_30_MIN: case SLEEP_30_MIN:
return 30UL * 60 * 1000; return 30UL * 60 * 1000;
} }
} }
int CrossPointSettings::getRefreshFrequency() const { int CrossPointSettings::getRefreshFrequency() const {
switch (refreshFrequency) { switch (refreshFrequency) {
case REFRESH_1: case REFRESH_1:
return 1; return 1;
case REFRESH_5: case REFRESH_5:
return 5; return 5;
case REFRESH_10: case REFRESH_10:
return 10; return 10;
case REFRESH_15: case REFRESH_15:
default: default:
return 15; return 15;
case REFRESH_30: case REFRESH_30:
return 30; return 30;
} }
} }
int CrossPointSettings::getReaderFontId() const { int CrossPointSettings::getReaderFontId() const {
switch (fontFamily) { switch (fontFamily) {
case BOOKERLY: case BOOKERLY:
default:
switch (fontSize) {
case SMALL:
return BOOKERLY_12_FONT_ID;
case MEDIUM:
default: default:
return BOOKERLY_14_FONT_ID; switch (fontSize) {
case LARGE: case SMALL:
return BOOKERLY_16_FONT_ID; return BOOKERLY_12_FONT_ID;
case EXTRA_LARGE: case MEDIUM:
return BOOKERLY_18_FONT_ID; default:
} return BOOKERLY_14_FONT_ID;
case NOTOSANS: case LARGE:
switch (fontSize) { return BOOKERLY_16_FONT_ID;
case SMALL: case EXTRA_LARGE:
return NOTOSANS_12_FONT_ID; return BOOKERLY_18_FONT_ID;
case MEDIUM: }
default: case NOTOSANS:
return NOTOSANS_14_FONT_ID; switch (fontSize) {
case LARGE: case SMALL:
return NOTOSANS_16_FONT_ID; return NOTOSANS_12_FONT_ID;
case EXTRA_LARGE: case MEDIUM:
return NOTOSANS_18_FONT_ID; default:
} return NOTOSANS_14_FONT_ID;
case OPENDYSLEXIC: case LARGE:
switch (fontSize) { return NOTOSANS_16_FONT_ID;
case SMALL: case EXTRA_LARGE:
return OPENDYSLEXIC_8_FONT_ID; return NOTOSANS_18_FONT_ID;
case MEDIUM: }
default: case OPENDYSLEXIC:
return OPENDYSLEXIC_10_FONT_ID; switch (fontSize) {
case LARGE: case SMALL:
return OPENDYSLEXIC_12_FONT_ID; return OPENDYSLEXIC_8_FONT_ID;
case EXTRA_LARGE: case MEDIUM:
return OPENDYSLEXIC_14_FONT_ID; default:
} return OPENDYSLEXIC_10_FONT_ID;
case LARGE:
return OPENDYSLEXIC_12_FONT_ID;
case EXTRA_LARGE:
return OPENDYSLEXIC_14_FONT_ID;
}
} }
} }

View File

@ -3,47 +3,36 @@
#include <iosfwd> #include <iosfwd>
class CrossPointSettings { class CrossPointSettings {
private: private:
// Private constructor for singleton // Private constructor for singleton
CrossPointSettings() = default; CrossPointSettings() = default;
// Static instance // Static instance
static CrossPointSettings instance; static CrossPointSettings instance;
public: public:
// Delete copy constructor and assignment // Delete copy constructor and assignment
CrossPointSettings(const CrossPointSettings &) = delete; CrossPointSettings(const CrossPointSettings&) = delete;
CrossPointSettings &operator=(const CrossPointSettings &) = delete; CrossPointSettings& operator=(const CrossPointSettings&) = delete;
// Should match with SettingsActivity text // Should match with SettingsActivity text
enum SLEEP_SCREEN_MODE { enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 };
DARK = 0,
LIGHT = 1,
CUSTOM = 2,
COVER = 3,
BLANK = 4
};
enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 };
// Status bar display type enum // Status bar display type enum
enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2 }; enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2 };
enum ORIENTATION { enum ORIENTATION {
PORTRAIT = 0, // 480x800 logical coordinates (current default) PORTRAIT = 0, // 480x800 logical coordinates (current default)
LANDSCAPE_CW = LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
1, // 800x480 logical coordinates, rotated 180° (swap top/bottom) INVERTED = 2, // 480x800 logical coordinates, inverted
INVERTED = 2, // 480x800 logical coordinates, inverted LANDSCAPE_CCW = 3 // 800x480 logical coordinates, native panel orientation
LANDSCAPE_CCW = 3 // 800x480 logical coordinates, native panel orientation
}; };
// Front button layout options // Front button layout options
// Default: Back, Confirm, Left, Right // Default: Back, Confirm, Left, Right
// Swapped: Left, Right, Back, Confirm // Swapped: Left, Right, Back, Confirm
enum FRONT_BUTTON_LAYOUT { enum FRONT_BUTTON_LAYOUT { BACK_CONFIRM_LEFT_RIGHT = 0, LEFT_RIGHT_BACK_CONFIRM = 1, LEFT_BACK_CONFIRM_RIGHT = 2 };
BACK_CONFIRM_LEFT_RIGHT = 0,
LEFT_RIGHT_BACK_CONFIRM = 1,
LEFT_BACK_CONFIRM_RIGHT = 2
};
// Side button layout options // Side button layout options
// Default: Previous, Next // Default: Previous, Next
@ -55,40 +44,19 @@ public:
// Font size options // Font size options
enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 }; enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 };
enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 }; enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 };
enum PARAGRAPH_ALIGNMENT { enum PARAGRAPH_ALIGNMENT { JUSTIFIED = 0, LEFT_ALIGN = 1, CENTER_ALIGN = 2, RIGHT_ALIGN = 3 };
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3
};
// Auto-sleep timeout options (in minutes) // Auto-sleep timeout options (in minutes)
enum SLEEP_TIMEOUT { enum SLEEP_TIMEOUT { SLEEP_1_MIN = 0, SLEEP_5_MIN = 1, SLEEP_10_MIN = 2, SLEEP_15_MIN = 3, SLEEP_30_MIN = 4 };
SLEEP_1_MIN = 0,
SLEEP_5_MIN = 1,
SLEEP_10_MIN = 2,
SLEEP_15_MIN = 3,
SLEEP_30_MIN = 4
};
// E-ink refresh frequency (pages between full refreshes) // E-ink refresh frequency (pages between full refreshes)
enum REFRESH_FREQUENCY { enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 };
REFRESH_1 = 0,
REFRESH_5 = 1,
REFRESH_10 = 2,
REFRESH_15 = 3,
REFRESH_30 = 4
};
// Short power button press actions // Short power button press actions
enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 }; enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 };
// Hide battery percentage // Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 };
HIDE_NEVER = 0,
HIDE_READER = 1,
HIDE_ALWAYS = 2
};
// Sleep screen settings // Sleep screen settings
uint8_t sleepScreen = DARK; uint8_t sleepScreen = DARK;
@ -132,7 +100,7 @@ public:
~CrossPointSettings() = default; ~CrossPointSettings() = default;
// Get singleton instance // Get singleton instance
static CrossPointSettings &getInstance() { return instance; } static CrossPointSettings& getInstance() { return instance; }
uint16_t getPowerButtonDuration() const { uint16_t getPowerButtonDuration() const {
return (shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) ? 10 : 400; return (shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) ? 10 : 400;

View File

@ -4,6 +4,7 @@
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <ThemeManager.h>
#include <Xtc.h> #include <Xtc.h>
#include <cstring> #include <cstring>
@ -17,17 +18,15 @@
#include "ScreenComponents.h" #include "ScreenComponents.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
#include <ThemeManager.h>
void HomeActivity::taskTrampoline(void *param) { void HomeActivity::taskTrampoline(void* param) {
auto *self = static_cast<HomeActivity *>(param); auto* self = static_cast<HomeActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
int HomeActivity::getMenuItemCount() const { int HomeActivity::getMenuItemCount() const {
int count = 3; // Browse Files, File Transfer, Settings int count = 3; // Browse Files, File Transfer, Settings
if (hasOpdsUrl) if (hasOpdsUrl) count++; // + Calibre Library
count++; // + Calibre Library
return count; return count;
} }
@ -43,8 +42,7 @@ void HomeActivity::onEnter() {
selectorIndex = 0; // Start at first item (first book if any, else first menu) selectorIndex = 0; // Start at first item (first book if any, else first menu)
// Check if we have a book to continue reading // Check if we have a book to continue reading
hasContinueReading = !APP_STATE.openEpubPath.empty() && hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str());
SdMan.exists(APP_STATE.openEpubPath.c_str());
// Check if OPDS browser URL is configured // Check if OPDS browser URL is configured
hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0;
@ -96,7 +94,7 @@ void HomeActivity::onEnter() {
} }
selectorIndex = 0; selectorIndex = 0;
lastBatteryCheck = 0; // Force update on first render lastBatteryCheck = 0; // Force update on first render
coverRendered = false; coverRendered = false;
coverBufferStored = false; coverBufferStored = false;
@ -107,25 +105,25 @@ void HomeActivity::onEnter() {
updateRequired = true; updateRequired = true;
xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask", xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask",
4096, // Stack size (increased for cover image rendering) 4096, // Stack size (increased for cover image rendering)
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
&displayTaskHandle // Task handle &displayTaskHandle // Task handle
); );
} }
void HomeActivity::loadRecentBooksData() { void HomeActivity::loadRecentBooksData() {
cachedRecentBooks.clear(); cachedRecentBooks.clear();
const auto &recentBooks = RECENT_BOOKS.getBooks(); const auto& recentBooks = RECENT_BOOKS.getBooks();
const int maxRecentBooks = 3; const int maxRecentBooks = 3;
int recentCount = std::min(static_cast<int>(recentBooks.size()), maxRecentBooks); int recentCount = std::min(static_cast<int>(recentBooks.size()), maxRecentBooks);
for (int i = 0; i < recentCount; i++) { for (int i = 0; i < recentCount; i++) {
const std::string &bookPath = recentBooks[i]; const std::string& bookPath = recentBooks[i];
CachedBookInfo info; CachedBookInfo info;
info.path = bookPath; // Store the full path info.path = bookPath; // Store the full path
// Extract title from path // Extract title from path
info.title = bookPath; info.title = bookPath;
size_t lastSlash = info.title.find_last_of('/'); size_t lastSlash = info.title.find_last_of('/');
@ -136,7 +134,7 @@ void HomeActivity::loadRecentBooksData() {
if (lastDot != std::string::npos) { if (lastDot != std::string::npos) {
info.title = info.title.substr(0, lastDot); info.title = info.title.substr(0, lastDot);
} }
if (StringUtils::checkFileExtension(bookPath, ".epub")) { if (StringUtils::checkFileExtension(bookPath, ".epub")) {
Epub epub(bookPath, "/.crosspoint"); Epub epub(bookPath, "/.crosspoint");
epub.load(false); epub.load(false);
@ -146,7 +144,7 @@ void HomeActivity::loadRecentBooksData() {
if (epub.generateThumbBmp()) { if (epub.generateThumbBmp()) {
info.coverPath = epub.getThumbBmpPath(); info.coverPath = epub.getThumbBmpPath();
} }
// Read progress // Read progress
FsFile f; FsFile f;
if (SdMan.openFileForRead("HOME", epub.getCachePath() + "/progress.bin", f)) { if (SdMan.openFileForRead("HOME", epub.getCachePath() + "/progress.bin", f)) {
@ -170,7 +168,7 @@ void HomeActivity::loadRecentBooksData() {
if (xtc.generateThumbBmp()) { if (xtc.generateThumbBmp()) {
info.coverPath = xtc.getThumbBmpPath(); info.coverPath = xtc.getThumbBmpPath();
} }
// Read progress // Read progress
FsFile f; FsFile f;
if (SdMan.openFileForRead("HOME", xtc.getCachePath() + "/progress.bin", f)) { if (SdMan.openFileForRead("HOME", xtc.getCachePath() + "/progress.bin", f)) {
@ -186,12 +184,12 @@ void HomeActivity::loadRecentBooksData() {
} }
} }
} }
Serial.printf("[HOME] Book %d: title='%s', cover='%s', progress=%d%%\n", Serial.printf("[HOME] Book %d: title='%s', cover='%s', progress=%d%%\n", i, info.title.c_str(),
i, info.title.c_str(), info.coverPath.c_str(), info.progressPercent); info.coverPath.c_str(), info.progressPercent);
cachedRecentBooks.push_back(info); cachedRecentBooks.push_back(info);
} }
Serial.printf("[HOME] Loaded %d recent books\n", (int)cachedRecentBooks.size()); Serial.printf("[HOME] Loaded %d recent books\n", (int)cachedRecentBooks.size());
} }
@ -213,7 +211,7 @@ void HomeActivity::onExit() {
} }
bool HomeActivity::storeCoverBuffer() { bool HomeActivity::storeCoverBuffer() {
uint8_t *frameBuffer = renderer.getFrameBuffer(); uint8_t* frameBuffer = renderer.getFrameBuffer();
if (!frameBuffer) { if (!frameBuffer) {
return false; return false;
} }
@ -222,7 +220,7 @@ bool HomeActivity::storeCoverBuffer() {
freeCoverBuffer(); freeCoverBuffer();
const size_t bufferSize = GfxRenderer::getBufferSize(); const size_t bufferSize = GfxRenderer::getBufferSize();
coverBuffer = static_cast<uint8_t *>(malloc(bufferSize)); coverBuffer = static_cast<uint8_t*>(malloc(bufferSize));
if (!coverBuffer) { if (!coverBuffer) {
return false; return false;
} }
@ -236,7 +234,7 @@ bool HomeActivity::restoreCoverBuffer() {
return false; return false;
} }
uint8_t *frameBuffer = renderer.getFrameBuffer(); uint8_t* frameBuffer = renderer.getFrameBuffer();
if (!frameBuffer) { if (!frameBuffer) {
return false; return false;
} }
@ -255,12 +253,10 @@ void HomeActivity::freeCoverBuffer() {
} }
void HomeActivity::loop() { void HomeActivity::loop() {
const bool prevPressed = const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Up) || mappedInput.wasPressed(MappedInputManager::Button::Left);
mappedInput.wasPressed(MappedInputManager::Button::Left); const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) ||
const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Right);
mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right);
const bool confirmPressed = mappedInput.wasReleased(MappedInputManager::Button::Confirm); const bool confirmPressed = mappedInput.wasReleased(MappedInputManager::Button::Confirm);
// Navigation uses theme-configured book slots (limited by actual books available) // Navigation uses theme-configured book slots (limited by actual books available)
@ -321,8 +317,7 @@ void HomeActivity::displayTaskLoop() {
void HomeActivity::render() { void HomeActivity::render() {
// Battery check logic (only update every 60 seconds) // Battery check logic (only update every 60 seconds)
const uint32_t now = millis(); const uint32_t now = millis();
const bool needBatteryUpdate = const bool needBatteryUpdate = (now - lastBatteryCheck > 60000) || (lastBatteryCheck == 0);
(now - lastBatteryCheck > 60000) || (lastBatteryCheck == 0);
if (needBatteryUpdate) { if (needBatteryUpdate) {
cachedBatteryLevel = battery.readPercentage(); cachedBatteryLevel = battery.readPercentage();
lastBatteryCheck = now; lastBatteryCheck = now;
@ -336,8 +331,7 @@ void HomeActivity::render() {
// --- Bind Global Data --- // --- Bind Global Data ---
context.setString("BatteryPercent", std::to_string(cachedBatteryLevel)); context.setString("BatteryPercent", std::to_string(cachedBatteryLevel));
context.setBool("ShowBatteryPercent", context.setBool("ShowBatteryPercent",
SETTINGS.hideBatteryPercentage != SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS);
CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS);
// --- Navigation counts (must match loop()) --- // --- Navigation counts (must match loop()) ---
const int recentCount = static_cast<int>(cachedRecentBooks.size()); const int recentCount = static_cast<int>(cachedRecentBooks.size());
@ -351,9 +345,9 @@ void HomeActivity::render() {
context.setInt("SelectedBookIndex", isBookSelected ? selectorIndex : -1); context.setInt("SelectedBookIndex", isBookSelected ? selectorIndex : -1);
for (int i = 0; i < recentCount; i++) { for (int i = 0; i < recentCount; i++) {
const auto &book = cachedRecentBooks[i]; const auto& book = cachedRecentBooks[i];
std::string prefix = "RecentBooks." + std::to_string(i) + "."; std::string prefix = "RecentBooks." + std::to_string(i) + ".";
context.setString(prefix + "Title", book.title); context.setString(prefix + "Title", book.title);
context.setString(prefix + "Image", book.coverPath); context.setString(prefix + "Image", book.coverPath);
context.setString(prefix + "Progress", std::to_string(book.progressPercent)); context.setString(prefix + "Progress", std::to_string(book.progressPercent));
@ -367,14 +361,13 @@ void HomeActivity::render() {
context.setString("BookTitle", lastBookTitle); context.setString("BookTitle", lastBookTitle);
context.setString("BookAuthor", lastBookAuthor); context.setString("BookAuthor", lastBookAuthor);
context.setString("BookCoverPath", coverBmpPath); context.setString("BookCoverPath", coverBmpPath);
context.setBool("HasCover", context.setBool("HasCover", hasContinueReading && hasCoverImage && !coverBmpPath.empty());
hasContinueReading && hasCoverImage && !coverBmpPath.empty());
context.setBool("ShowInfoBox", true); context.setBool("ShowInfoBox", true);
// --- Main Menu Data --- // --- Main Menu Data ---
// Menu items start after the book slot // Menu items start after the book slot
const int menuStartIdx = navBookCount; const int menuStartIdx = navBookCount;
int idx = 0; int idx = 0;
const int myLibraryIdx = menuStartIdx + idx++; const int myLibraryIdx = menuStartIdx + idx++;
const int opdsLibraryIdx = hasOpdsUrl ? menuStartIdx + idx++ : -1; const int opdsLibraryIdx = hasOpdsUrl ? menuStartIdx + idx++ : -1;

View File

@ -11,7 +11,7 @@
// Cached data for a recent book // Cached data for a recent book
struct CachedBookInfo { struct CachedBookInfo {
std::string path; // Full path to the book file std::string path; // Full path to the book file
std::string title; std::string title;
std::string coverPath; std::string coverPath;
int progressPercent = 0; int progressPercent = 0;
@ -25,15 +25,15 @@ class HomeActivity final : public Activity {
bool hasContinueReading = false; bool hasContinueReading = false;
bool hasOpdsUrl = false; bool hasOpdsUrl = false;
bool hasCoverImage = false; bool hasCoverImage = false;
bool coverRendered = false; // Track if cover has been rendered once bool coverRendered = false; // Track if cover has been rendered once
bool coverBufferStored = false; // Track if cover buffer is stored bool coverBufferStored = false; // Track if cover buffer is stored
uint8_t *coverBuffer = nullptr; // HomeActivity's own buffer for cover image uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
std::string lastBookTitle; std::string lastBookTitle;
std::string lastBookAuthor; std::string lastBookAuthor;
std::string coverBmpPath; std::string coverBmpPath;
uint8_t cachedBatteryLevel = 0; uint8_t cachedBatteryLevel = 0;
uint32_t lastBatteryCheck = 0; uint32_t lastBatteryCheck = 0;
// Cached recent books data (loaded once in onEnter) // Cached recent books data (loaded once in onEnter)
std::vector<CachedBookInfo> cachedRecentBooks; std::vector<CachedBookInfo> cachedRecentBooks;
const std::function<void()> onContinueReading; const std::function<void()> onContinueReading;
@ -42,25 +42,25 @@ class HomeActivity final : public Activity {
const std::function<void()> onFileTransferOpen; const std::function<void()> onFileTransferOpen;
const std::function<void()> onOpdsBrowserOpen; const std::function<void()> onOpdsBrowserOpen;
static void taskTrampoline(void *param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void render(); void render();
int getMenuItemCount() const; int getMenuItemCount() const;
bool storeCoverBuffer(); // Store frame buffer for cover image bool storeCoverBuffer(); // Store frame buffer for cover image
bool restoreCoverBuffer(); // Restore frame buffer from stored cover bool restoreCoverBuffer(); // Restore frame buffer from stored cover
void freeCoverBuffer(); // Free the stored cover buffer void freeCoverBuffer(); // Free the stored cover buffer
void loadRecentBooksData(); // Load and cache recent books data void loadRecentBooksData(); // Load and cache recent books data
public: public:
explicit HomeActivity(GfxRenderer &renderer, MappedInputManager &mappedInput, explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()> &onContinueReading, const std::function<void()>& onContinueReading, const std::function<void()>& onMyLibraryOpen,
const std::function<void()> &onMyLibraryOpen, const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
const std::function<void()> &onSettingsOpen, const std::function<void()>& onOpdsBrowserOpen)
const std::function<void()> &onFileTransferOpen,
const std::function<void()> &onOpdsBrowserOpen)
: Activity("Home", renderer, mappedInput), : Activity("Home", renderer, mappedInput),
onContinueReading(onContinueReading), onMyLibraryOpen(onMyLibraryOpen), onContinueReading(onContinueReading),
onSettingsOpen(onSettingsOpen), onFileTransferOpen(onFileTransferOpen), onMyLibraryOpen(onMyLibraryOpen),
onSettingsOpen(onSettingsOpen),
onFileTransferOpen(onFileTransferOpen),
onOpdsBrowserOpen(onOpdsBrowserOpen) {} onOpdsBrowserOpen(onOpdsBrowserOpen) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;

View File

@ -18,8 +18,8 @@
// ... // ...
void CategorySettingsActivity::taskTrampoline(void *param) { void CategorySettingsActivity::taskTrampoline(void* param) {
auto *self = static_cast<CategorySettingsActivity *>(param); auto* self = static_cast<CategorySettingsActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
@ -30,8 +30,7 @@ void CategorySettingsActivity::onEnter() {
selectedSettingIndex = 0; selectedSettingIndex = 0;
updateRequired = true; updateRequired = true;
xTaskCreate(&CategorySettingsActivity::taskTrampoline, xTaskCreate(&CategorySettingsActivity::taskTrampoline, "CategorySettingsActivityTask", 4096, this, 1,
"CategorySettingsActivityTask", 4096, this, 1,
&displayTaskHandle); &displayTaskHandle);
} }
@ -71,15 +70,11 @@ void CategorySettingsActivity::loop() {
// Handle navigation // Handle navigation
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedSettingIndex = (selectedSettingIndex > 0) selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount - 1);
? (selectedSettingIndex - 1)
: (settingsCount - 1);
updateRequired = true; updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) selectedSettingIndex = (selectedSettingIndex < settingsCount - 1) ? (selectedSettingIndex + 1) : 0;
? (selectedSettingIndex + 1)
: 0;
updateRequired = true; updateRequired = true;
} }
} }
@ -89,7 +84,7 @@ void CategorySettingsActivity::toggleCurrentSetting() {
return; return;
} }
const auto &setting = settingsList[selectedSettingIndex]; const auto& setting = settingsList[selectedSettingIndex];
if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) { if (setting.type == SettingType::TOGGLE && setting.valuePtr != nullptr) {
// Toggle the boolean value using the member pointer // Toggle the boolean value using the member pointer
@ -97,10 +92,8 @@ void CategorySettingsActivity::toggleCurrentSetting() {
SETTINGS.*(setting.valuePtr) = !currentValue; SETTINGS.*(setting.valuePtr) = !currentValue;
} else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) { } else if (setting.type == SettingType::ENUM && setting.valuePtr != nullptr) {
const uint8_t currentValue = SETTINGS.*(setting.valuePtr); const uint8_t currentValue = SETTINGS.*(setting.valuePtr);
SETTINGS.*(setting.valuePtr) = SETTINGS.*(setting.valuePtr) = (currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size());
(currentValue + 1) % static_cast<uint8_t>(setting.enumValues.size()); } else if (setting.type == SettingType::VALUE && setting.valuePtr != nullptr) {
} else if (setting.type == SettingType::VALUE &&
setting.valuePtr != nullptr) {
const int8_t currentValue = SETTINGS.*(setting.valuePtr); const int8_t currentValue = SETTINGS.*(setting.valuePtr);
if (currentValue + setting.valueRange.step > setting.valueRange.max) { if (currentValue + setting.valueRange.step > setting.valueRange.max) {
SETTINGS.*(setting.valuePtr) = setting.valueRange.min; SETTINGS.*(setting.valuePtr) = setting.valueRange.min;
@ -111,20 +104,18 @@ void CategorySettingsActivity::toggleCurrentSetting() {
if (strcmp(setting.name, "KOReader Sync") == 0) { if (strcmp(setting.name, "KOReader Sync") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity( enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
new KOReaderSettingsActivity(renderer, mappedInput, [this] { exitActivity();
exitActivity(); updateRequired = true;
updateRequired = true; }));
}));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Calibre Settings") == 0) { } else if (strcmp(setting.name, "Calibre Settings") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity( enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
new CalibreSettingsActivity(renderer, mappedInput, [this] { exitActivity();
exitActivity(); updateRequired = true;
updateRequired = true; }));
}));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} else if (strcmp(setting.name, "Clear Cache") == 0) { } else if (strcmp(setting.name, "Clear Cache") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -145,11 +136,10 @@ void CategorySettingsActivity::toggleCurrentSetting() {
} else if (strcmp(setting.name, "Theme") == 0) { } else if (strcmp(setting.name, "Theme") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
enterNewActivity( enterNewActivity(new ThemeSelectionActivity(renderer, mappedInput, [this] {
new ThemeSelectionActivity(renderer, mappedInput, [this] { exitActivity();
exitActivity(); updateRequired = true;
updateRequired = true; }));
}));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
} else { } else {
@ -177,51 +167,41 @@ void CategorySettingsActivity::render() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, renderer.drawCenteredText(UI_12_FONT_ID, 15, categoryName, true, EpdFontFamily::BOLD);
EpdFontFamily::BOLD);
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30);
// Draw all settings // Draw all settings
for (int i = 0; i < settingsCount; i++) { for (int i = 0; i < settingsCount; i++) {
const int settingY = 60 + i * 30; // 30 pixels between settings const int settingY = 60 + i * 30; // 30 pixels between settings
const bool isSelected = (i == selectedSettingIndex); const bool isSelected = (i == selectedSettingIndex);
// Draw setting name // Draw setting name
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected);
!isSelected);
// Draw value based on setting type // Draw value based on setting type
std::string valueText; std::string valueText;
if (settingsList[i].type == SettingType::TOGGLE && if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
settingsList[i].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr); const bool value = SETTINGS.*(settingsList[i].valuePtr);
valueText = value ? "ON" : "OFF"; valueText = value ? "ON" : "OFF";
} else if (settingsList[i].type == SettingType::ENUM && } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
settingsList[i].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
valueText = settingsList[i].enumValues[value]; valueText = settingsList[i].enumValues[value];
} else if (settingsList[i].type == SettingType::VALUE && } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
settingsList[i].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
} }
if (!valueText.empty()) { if (!valueText.empty()) {
const auto width = const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY,
valueText.c_str(), !isSelected);
} }
} }
renderer.drawText( renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
SMALL_FONT_ID, pageHeight - 60, CROSSPOINT_VERSION);
pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", ""); const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -8,76 +8,53 @@
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "fontIds.h" #include "fontIds.h"
const char *SettingsActivity::categoryNames[categoryCount] = { const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
"Display", "Reader", "Controls", "System"};
namespace { namespace {
constexpr int displaySettingsCount = 6; constexpr int displaySettingsCount = 6;
const SettingInfo displaySettings[displaySettingsCount] = { const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
{"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}),
SettingInfo::Enum("Sleep Screen Cover Mode", SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}),
&CrossPointSettings::sleepScreenCoverMode, SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}),
{"Fit", "Crop"}), SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
{"None", "No Progress", "Full"}),
SettingInfo::Enum("Hide Battery %",
&CrossPointSettings::hideBatteryPercentage,
{"Never", "In Reader", "Always"}),
SettingInfo::Enum(
"Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::Action("Theme")}; SettingInfo::Action("Theme")};
constexpr int readerSettingsCount = 9; constexpr int readerSettingsCount = 9;
const SettingInfo readerSettings[readerSettingsCount] = { const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
{"Bookerly", "Noto Sans", "Open Dyslexic"}), SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}),
{"Small", "Medium", "Large", "X Large"}), SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}),
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment,
{"Tight", "Normal", "Wide"}),
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin,
{5, 40, 5}),
SettingInfo::Enum("Paragraph Alignment",
&CrossPointSettings::paragraphAlignment,
{"Justify", "Left", "Center", "Right"}), {"Justify", "Left", "Center", "Right"}),
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled),
SettingInfo::Enum( SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
"Reading Orientation", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("Extra Paragraph Spacing", SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)};
&CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("Text Anti-Aliasing",
&CrossPointSettings::textAntiAliasing)};
constexpr int controlsSettingsCount = 4; constexpr int controlsSettingsCount = 4;
const SettingInfo controlsSettings[controlsSettingsCount] = { const SettingInfo controlsSettings[controlsSettingsCount] = {
SettingInfo::Enum("Front Button Layout", SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout,
&CrossPointSettings::frontButtonLayout, {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}),
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
"Lft, Bck, Cnfrm, Rght"}),
SettingInfo::Enum("Side Button Layout (reader)",
&CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}), {"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
&CrossPointSettings::longPressChapterSkip), SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})};
SettingInfo::Enum("Short Power Button Click",
&CrossPointSettings::shortPwrBtn,
{"Ignore", "Sleep", "Page Turn"})};
constexpr int systemSettingsCount = 5; constexpr int systemSettingsCount = 5;
const SettingInfo systemSettings[systemSettingsCount] = { const SettingInfo systemSettings[systemSettingsCount] = {
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}), {"1 min", "5 min", "10 min", "15 min", "30 min"}),
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"),
SettingInfo::Action("Check for updates")}; SettingInfo::Action("Check for updates")};
} // namespace } // namespace
void SettingsActivity::taskTrampoline(void *param) { void SettingsActivity::taskTrampoline(void* param) {
auto *self = static_cast<SettingsActivity *>(param); auto* self = static_cast<SettingsActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
@ -92,10 +69,10 @@ void SettingsActivity::onEnter() {
updateRequired = true; updateRequired = true;
xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask", xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask",
4096, // Stack size 4096, // Stack size
this, // Parameters this, // Parameters
1, // Priority 1, // Priority
&displayTaskHandle // Task handle &displayTaskHandle // Task handle
); );
} }
@ -135,16 +112,12 @@ void SettingsActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
// Move selection up (with wrap-around) // Move selection up (with wrap-around)
selectedCategoryIndex = (selectedCategoryIndex > 0) selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1);
? (selectedCategoryIndex - 1)
: (categoryCount - 1);
updateRequired = true; updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
// Move selection down (with wrap around) // Move selection down (with wrap around)
selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0;
? (selectedCategoryIndex + 1)
: 0;
updateRequired = true; updateRequired = true;
} }
} }
@ -157,34 +130,33 @@ void SettingsActivity::enterCategory(int categoryIndex) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity(); exitActivity();
const SettingInfo *settingsList = nullptr; const SettingInfo* settingsList = nullptr;
int settingsCount = 0; int settingsCount = 0;
switch (categoryIndex) { switch (categoryIndex) {
case 0: // Display case 0: // Display
settingsList = displaySettings; settingsList = displaySettings;
settingsCount = displaySettingsCount; settingsCount = displaySettingsCount;
break; break;
case 1: // Reader case 1: // Reader
settingsList = readerSettings; settingsList = readerSettings;
settingsCount = readerSettingsCount; settingsCount = readerSettingsCount;
break; break;
case 2: // Controls case 2: // Controls
settingsList = controlsSettings; settingsList = controlsSettings;
settingsCount = controlsSettingsCount; settingsCount = controlsSettingsCount;
break; break;
case 3: // System case 3: // System
settingsList = systemSettings; settingsList = systemSettings;
settingsCount = systemSettingsCount; settingsCount = systemSettingsCount;
break; break;
} }
enterNewActivity(new CategorySettingsActivity( enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList,
renderer, mappedInput, categoryNames[categoryIndex], settingsList, settingsCount, [this] {
settingsCount, [this] { exitActivity();
exitActivity(); updateRequired = true;
updateRequired = true; }));
}));
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
} }
@ -207,31 +179,26 @@ void SettingsActivity::render() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD);
EpdFontFamily::BOLD);
// Draw selection // Draw selection
renderer.fillRect(0, 60 + selectedCategoryIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, 60 + selectedCategoryIndex * 30 - 2, pageWidth - 1, 30);
// Draw all categories // Draw all categories
for (int i = 0; i < categoryCount; i++) { for (int i = 0; i < categoryCount; i++) {
const int categoryY = 60 + i * 30; // 30 pixels between categories const int categoryY = 60 + i * 30; // 30 pixels between categories
// Draw category name // Draw category name
renderer.drawText(UI_10_FONT_ID, 20, categoryY, categoryNames[i], renderer.drawText(UI_10_FONT_ID, 20, categoryY, categoryNames[i], i != selectedCategoryIndex);
i != selectedCategoryIndex);
} }
// Draw version text above button hints // Draw version text above button hints
renderer.drawText( renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
SMALL_FONT_ID, pageHeight - 60, CROSSPOINT_VERSION);
pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION);
// Draw help text // Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels("« Back", "Select", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
labels.btn4);
// Always use standard refresh for settings screen // Always use standard refresh for settings screen
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -1,14 +1,17 @@
#include "ThemeSelectionActivity.h" #include "ThemeSelectionActivity.h"
#include <GfxRenderer.h>
#include <SDCardManager.h>
#include <esp_system.h>
#include <cstring>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "fontIds.h" #include "fontIds.h"
#include <GfxRenderer.h>
#include <SDCardManager.h>
#include <cstring>
#include <esp_system.h>
void ThemeSelectionActivity::taskTrampoline(void *param) { void ThemeSelectionActivity::taskTrampoline(void* param) {
auto *self = static_cast<ThemeSelectionActivity *>(param); auto* self = static_cast<ThemeSelectionActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
} }
@ -49,8 +52,7 @@ void ThemeSelectionActivity::onEnter() {
} }
updateRequired = true; updateRequired = true;
xTaskCreate(&ThemeSelectionActivity::taskTrampoline, "ThemeSelTask", 4096, xTaskCreate(&ThemeSelectionActivity::taskTrampoline, "ThemeSelTask", 4096, this, 1, &displayTaskHandle);
this, 1, &displayTaskHandle);
} }
void ThemeSelectionActivity::onExit() { void ThemeSelectionActivity::onExit() {
@ -72,17 +74,14 @@ void ThemeSelectionActivity::loop() {
// Only reboot if theme actually changed // Only reboot if theme actually changed
if (selected != std::string(SETTINGS.themeName)) { if (selected != std::string(SETTINGS.themeName)) {
strncpy(SETTINGS.themeName, selected.c_str(), strncpy(SETTINGS.themeName, selected.c_str(), sizeof(SETTINGS.themeName) - 1);
sizeof(SETTINGS.themeName) - 1);
SETTINGS.themeName[sizeof(SETTINGS.themeName) - 1] = '\0'; SETTINGS.themeName[sizeof(SETTINGS.themeName) - 1] = '\0';
SETTINGS.saveToFile(); SETTINGS.saveToFile();
// Show reboot message // Show reboot message
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, renderer.getScreenHeight() / 2 - 20, renderer.drawCenteredText(UI_12_FONT_ID, renderer.getScreenHeight() / 2 - 20, "Applying theme...", true);
"Applying theme...", true); renderer.drawCenteredText(UI_10_FONT_ID, renderer.getScreenHeight() / 2 + 10, "Device will restart", true);
renderer.drawCenteredText(UI_10_FONT_ID, renderer.getScreenHeight() / 2 + 10,
"Device will restart", true);
renderer.displayBuffer(); renderer.displayBuffer();
// Small delay to ensure display updates // Small delay to ensure display updates
@ -103,13 +102,11 @@ void ThemeSelectionActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = selectedIndex = (selectedIndex > 0) ? (selectedIndex - 1) : (themeNames.size() - 1);
(selectedIndex > 0) ? (selectedIndex - 1) : (themeNames.size() - 1);
updateRequired = true; updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = selectedIndex = (selectedIndex < themeNames.size() - 1) ? (selectedIndex + 1) : 0;
(selectedIndex < themeNames.size() - 1) ? (selectedIndex + 1) : 0;
updateRequired = true; updateRequired = true;
} }
} }
@ -133,8 +130,7 @@ void ThemeSelectionActivity::render() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Header // Header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Theme", true, renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Theme", true, EpdFontFamily::BOLD);
EpdFontFamily::BOLD);
// Layout constants // Layout constants
const int entryHeight = 30; const int entryHeight = 30;
@ -155,8 +151,7 @@ void ThemeSelectionActivity::render() const {
// Draw Highlight // Draw Highlight
int visibleIndex = selectedIndex - startIdx; int visibleIndex = selectedIndex - startIdx;
if (visibleIndex >= 0 && visibleIndex < maxVisible) { if (visibleIndex >= 0 && visibleIndex < maxVisible) {
renderer.fillRect(0, startY + visibleIndex * entryHeight - 2, pageWidth - 1, renderer.fillRect(0, startY + visibleIndex * entryHeight - 2, pageWidth - 1, entryHeight);
entryHeight);
} }
// Draw List // Draw List
@ -176,15 +171,13 @@ void ThemeSelectionActivity::render() const {
if (themeNames.size() > maxVisible) { if (themeNames.size() > maxVisible) {
int barHeight = pageHeight - startY - 40; int barHeight = pageHeight - startY - 40;
int thumbHeight = barHeight * maxVisible / themeNames.size(); int thumbHeight = barHeight * maxVisible / themeNames.size();
int thumbY = startY + (barHeight - thumbHeight) * startIdx / int thumbY = startY + (barHeight - thumbHeight) * startIdx / (themeNames.size() - maxVisible);
(themeNames.size() - maxVisible);
renderer.fillRect(pageWidth - 5, startY, 2, barHeight, 0); renderer.fillRect(pageWidth - 5, startY, 2, barHeight, 0);
renderer.fillRect(pageWidth - 7, thumbY, 6, thumbHeight, 1); renderer.fillRect(pageWidth - 7, thumbY, 6, thumbHeight, 1);
} }
const auto labels = mappedInput.mapLabels("Cancel", "Select", "", ""); const auto labels = mappedInput.mapLabels("Cancel", "Select", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -1,12 +1,14 @@
#pragma once #pragma once
#include "activities/Activity.h"
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <functional> #include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
#include "activities/Activity.h"
class ThemeSelectionActivity final : public Activity { class ThemeSelectionActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
@ -15,13 +17,12 @@ class ThemeSelectionActivity final : public Activity {
std::vector<std::string> themeNames; std::vector<std::string> themeNames;
const std::function<void()> onGoBack; const std::function<void()> onGoBack;
static void taskTrampoline(void *param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();
void render() const; void render() const;
public: public:
ThemeSelectionActivity(GfxRenderer &renderer, MappedInputManager &mappedInput, ThemeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::function<void()>& onGoBack)
const std::function<void()> &onGoBack)
: Activity("ThemeSelection", renderer, mappedInput), onGoBack(onGoBack) {} : Activity("ThemeSelection", renderer, mappedInput), onGoBack(onGoBack) {}
void onEnter() override; void onEnter() override;
void onExit() override; void onExit() override;

View File

@ -29,14 +29,14 @@
#define SPI_FQ 40000000 #define SPI_FQ 40000000
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) // Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
#define EPD_SCLK 8 // SPI Clock #define EPD_SCLK 8 // SPI Clock
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) #define EPD_MOSI 10 // SPI MOSI (Master Out Slave In)
#define EPD_CS 21 // Chip Select #define EPD_CS 21 // Chip Select
#define EPD_DC 4 // Data/Command #define EPD_DC 4 // Data/Command
#define EPD_RST 5 // Reset #define EPD_RST 5 // Reset
#define EPD_BUSY 6 // Busy #define EPD_BUSY 6 // Busy
#define UART0_RXD 20 // Used for USB connection detection #define UART0_RXD 20 // Used for USB connection detection
#define SD_SPI_MISO 7 #define SD_SPI_MISO 7
@ -44,101 +44,85 @@ EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY);
InputManager inputManager; InputManager inputManager;
MappedInputManager mappedInputManager(inputManager); MappedInputManager mappedInputManager(inputManager);
GfxRenderer renderer(einkDisplay); GfxRenderer renderer(einkDisplay);
Activity *currentActivity; Activity* currentActivity;
// Fonts // Fonts
EpdFont bookerly14RegularFont(&bookerly_14_regular); EpdFont bookerly14RegularFont(&bookerly_14_regular);
EpdFont bookerly14BoldFont(&bookerly_14_bold); EpdFont bookerly14BoldFont(&bookerly_14_bold);
EpdFont bookerly14ItalicFont(&bookerly_14_italic); EpdFont bookerly14ItalicFont(&bookerly_14_italic);
EpdFont bookerly14BoldItalicFont(&bookerly_14_bolditalic); EpdFont bookerly14BoldItalicFont(&bookerly_14_bolditalic);
EpdFontFamily bookerly14FontFamily(&bookerly14RegularFont, &bookerly14BoldFont, EpdFontFamily bookerly14FontFamily(&bookerly14RegularFont, &bookerly14BoldFont, &bookerly14ItalicFont,
&bookerly14ItalicFont,
&bookerly14BoldItalicFont); &bookerly14BoldItalicFont);
#ifndef OMIT_FONTS #ifndef OMIT_FONTS
EpdFont bookerly12RegularFont(&bookerly_12_regular); EpdFont bookerly12RegularFont(&bookerly_12_regular);
EpdFont bookerly12BoldFont(&bookerly_12_bold); EpdFont bookerly12BoldFont(&bookerly_12_bold);
EpdFont bookerly12ItalicFont(&bookerly_12_italic); EpdFont bookerly12ItalicFont(&bookerly_12_italic);
EpdFont bookerly12BoldItalicFont(&bookerly_12_bolditalic); EpdFont bookerly12BoldItalicFont(&bookerly_12_bolditalic);
EpdFontFamily bookerly12FontFamily(&bookerly12RegularFont, &bookerly12BoldFont, EpdFontFamily bookerly12FontFamily(&bookerly12RegularFont, &bookerly12BoldFont, &bookerly12ItalicFont,
&bookerly12ItalicFont,
&bookerly12BoldItalicFont); &bookerly12BoldItalicFont);
EpdFont bookerly16RegularFont(&bookerly_16_regular); EpdFont bookerly16RegularFont(&bookerly_16_regular);
EpdFont bookerly16BoldFont(&bookerly_16_bold); EpdFont bookerly16BoldFont(&bookerly_16_bold);
EpdFont bookerly16ItalicFont(&bookerly_16_italic); EpdFont bookerly16ItalicFont(&bookerly_16_italic);
EpdFont bookerly16BoldItalicFont(&bookerly_16_bolditalic); EpdFont bookerly16BoldItalicFont(&bookerly_16_bolditalic);
EpdFontFamily bookerly16FontFamily(&bookerly16RegularFont, &bookerly16BoldFont, EpdFontFamily bookerly16FontFamily(&bookerly16RegularFont, &bookerly16BoldFont, &bookerly16ItalicFont,
&bookerly16ItalicFont,
&bookerly16BoldItalicFont); &bookerly16BoldItalicFont);
EpdFont bookerly18RegularFont(&bookerly_18_regular); EpdFont bookerly18RegularFont(&bookerly_18_regular);
EpdFont bookerly18BoldFont(&bookerly_18_bold); EpdFont bookerly18BoldFont(&bookerly_18_bold);
EpdFont bookerly18ItalicFont(&bookerly_18_italic); EpdFont bookerly18ItalicFont(&bookerly_18_italic);
EpdFont bookerly18BoldItalicFont(&bookerly_18_bolditalic); EpdFont bookerly18BoldItalicFont(&bookerly_18_bolditalic);
EpdFontFamily bookerly18FontFamily(&bookerly18RegularFont, &bookerly18BoldFont, EpdFontFamily bookerly18FontFamily(&bookerly18RegularFont, &bookerly18BoldFont, &bookerly18ItalicFont,
&bookerly18ItalicFont,
&bookerly18BoldItalicFont); &bookerly18BoldItalicFont);
EpdFont notosans12RegularFont(&notosans_12_regular); EpdFont notosans12RegularFont(&notosans_12_regular);
EpdFont notosans12BoldFont(&notosans_12_bold); EpdFont notosans12BoldFont(&notosans_12_bold);
EpdFont notosans12ItalicFont(&notosans_12_italic); EpdFont notosans12ItalicFont(&notosans_12_italic);
EpdFont notosans12BoldItalicFont(&notosans_12_bolditalic); EpdFont notosans12BoldItalicFont(&notosans_12_bolditalic);
EpdFontFamily notosans12FontFamily(&notosans12RegularFont, &notosans12BoldFont, EpdFontFamily notosans12FontFamily(&notosans12RegularFont, &notosans12BoldFont, &notosans12ItalicFont,
&notosans12ItalicFont,
&notosans12BoldItalicFont); &notosans12BoldItalicFont);
EpdFont notosans14RegularFont(&notosans_14_regular); EpdFont notosans14RegularFont(&notosans_14_regular);
EpdFont notosans14BoldFont(&notosans_14_bold); EpdFont notosans14BoldFont(&notosans_14_bold);
EpdFont notosans14ItalicFont(&notosans_14_italic); EpdFont notosans14ItalicFont(&notosans_14_italic);
EpdFont notosans14BoldItalicFont(&notosans_14_bolditalic); EpdFont notosans14BoldItalicFont(&notosans_14_bolditalic);
EpdFontFamily notosans14FontFamily(&notosans14RegularFont, &notosans14BoldFont, EpdFontFamily notosans14FontFamily(&notosans14RegularFont, &notosans14BoldFont, &notosans14ItalicFont,
&notosans14ItalicFont,
&notosans14BoldItalicFont); &notosans14BoldItalicFont);
EpdFont notosans16RegularFont(&notosans_16_regular); EpdFont notosans16RegularFont(&notosans_16_regular);
EpdFont notosans16BoldFont(&notosans_16_bold); EpdFont notosans16BoldFont(&notosans_16_bold);
EpdFont notosans16ItalicFont(&notosans_16_italic); EpdFont notosans16ItalicFont(&notosans_16_italic);
EpdFont notosans16BoldItalicFont(&notosans_16_bolditalic); EpdFont notosans16BoldItalicFont(&notosans_16_bolditalic);
EpdFontFamily notosans16FontFamily(&notosans16RegularFont, &notosans16BoldFont, EpdFontFamily notosans16FontFamily(&notosans16RegularFont, &notosans16BoldFont, &notosans16ItalicFont,
&notosans16ItalicFont,
&notosans16BoldItalicFont); &notosans16BoldItalicFont);
EpdFont notosans18RegularFont(&notosans_18_regular); EpdFont notosans18RegularFont(&notosans_18_regular);
EpdFont notosans18BoldFont(&notosans_18_bold); EpdFont notosans18BoldFont(&notosans_18_bold);
EpdFont notosans18ItalicFont(&notosans_18_italic); EpdFont notosans18ItalicFont(&notosans_18_italic);
EpdFont notosans18BoldItalicFont(&notosans_18_bolditalic); EpdFont notosans18BoldItalicFont(&notosans_18_bolditalic);
EpdFontFamily notosans18FontFamily(&notosans18RegularFont, &notosans18BoldFont, EpdFontFamily notosans18FontFamily(&notosans18RegularFont, &notosans18BoldFont, &notosans18ItalicFont,
&notosans18ItalicFont,
&notosans18BoldItalicFont); &notosans18BoldItalicFont);
EpdFont opendyslexic8RegularFont(&opendyslexic_8_regular); EpdFont opendyslexic8RegularFont(&opendyslexic_8_regular);
EpdFont opendyslexic8BoldFont(&opendyslexic_8_bold); EpdFont opendyslexic8BoldFont(&opendyslexic_8_bold);
EpdFont opendyslexic8ItalicFont(&opendyslexic_8_italic); EpdFont opendyslexic8ItalicFont(&opendyslexic_8_italic);
EpdFont opendyslexic8BoldItalicFont(&opendyslexic_8_bolditalic); EpdFont opendyslexic8BoldItalicFont(&opendyslexic_8_bolditalic);
EpdFontFamily opendyslexic8FontFamily(&opendyslexic8RegularFont, EpdFontFamily opendyslexic8FontFamily(&opendyslexic8RegularFont, &opendyslexic8BoldFont, &opendyslexic8ItalicFont,
&opendyslexic8BoldFont,
&opendyslexic8ItalicFont,
&opendyslexic8BoldItalicFont); &opendyslexic8BoldItalicFont);
EpdFont opendyslexic10RegularFont(&opendyslexic_10_regular); EpdFont opendyslexic10RegularFont(&opendyslexic_10_regular);
EpdFont opendyslexic10BoldFont(&opendyslexic_10_bold); EpdFont opendyslexic10BoldFont(&opendyslexic_10_bold);
EpdFont opendyslexic10ItalicFont(&opendyslexic_10_italic); EpdFont opendyslexic10ItalicFont(&opendyslexic_10_italic);
EpdFont opendyslexic10BoldItalicFont(&opendyslexic_10_bolditalic); EpdFont opendyslexic10BoldItalicFont(&opendyslexic_10_bolditalic);
EpdFontFamily opendyslexic10FontFamily(&opendyslexic10RegularFont, EpdFontFamily opendyslexic10FontFamily(&opendyslexic10RegularFont, &opendyslexic10BoldFont, &opendyslexic10ItalicFont,
&opendyslexic10BoldFont,
&opendyslexic10ItalicFont,
&opendyslexic10BoldItalicFont); &opendyslexic10BoldItalicFont);
EpdFont opendyslexic12RegularFont(&opendyslexic_12_regular); EpdFont opendyslexic12RegularFont(&opendyslexic_12_regular);
EpdFont opendyslexic12BoldFont(&opendyslexic_12_bold); EpdFont opendyslexic12BoldFont(&opendyslexic_12_bold);
EpdFont opendyslexic12ItalicFont(&opendyslexic_12_italic); EpdFont opendyslexic12ItalicFont(&opendyslexic_12_italic);
EpdFont opendyslexic12BoldItalicFont(&opendyslexic_12_bolditalic); EpdFont opendyslexic12BoldItalicFont(&opendyslexic_12_bolditalic);
EpdFontFamily opendyslexic12FontFamily(&opendyslexic12RegularFont, EpdFontFamily opendyslexic12FontFamily(&opendyslexic12RegularFont, &opendyslexic12BoldFont, &opendyslexic12ItalicFont,
&opendyslexic12BoldFont,
&opendyslexic12ItalicFont,
&opendyslexic12BoldItalicFont); &opendyslexic12BoldItalicFont);
EpdFont opendyslexic14RegularFont(&opendyslexic_14_regular); EpdFont opendyslexic14RegularFont(&opendyslexic_14_regular);
EpdFont opendyslexic14BoldFont(&opendyslexic_14_bold); EpdFont opendyslexic14BoldFont(&opendyslexic_14_bold);
EpdFont opendyslexic14ItalicFont(&opendyslexic_14_italic); EpdFont opendyslexic14ItalicFont(&opendyslexic_14_italic);
EpdFont opendyslexic14BoldItalicFont(&opendyslexic_14_bolditalic); EpdFont opendyslexic14BoldItalicFont(&opendyslexic_14_bolditalic);
EpdFontFamily opendyslexic14FontFamily(&opendyslexic14RegularFont, EpdFontFamily opendyslexic14FontFamily(&opendyslexic14RegularFont, &opendyslexic14BoldFont, &opendyslexic14ItalicFont,
&opendyslexic14BoldFont,
&opendyslexic14ItalicFont,
&opendyslexic14BoldItalicFont); &opendyslexic14BoldItalicFont);
#endif // OMIT_FONTS #endif // OMIT_FONTS
EpdFont smallFont(&notosans_8_regular); EpdFont smallFont(&notosans_8_regular);
EpdFontFamily smallFontFamily(&smallFont); EpdFontFamily smallFontFamily(&smallFont);
@ -163,7 +147,7 @@ void exitActivity() {
} }
} }
void enterNewActivity(Activity *activity) { void enterNewActivity(Activity* activity) {
currentActivity = activity; currentActivity = activity;
currentActivity->onEnter(); currentActivity->onEnter();
} }
@ -180,16 +164,13 @@ void verifyWakeupLongPress() {
// now from millis()==0 (i.e. device start time). // now from millis()==0 (i.e. device start time).
const uint16_t calibration = start; const uint16_t calibration = start;
const uint16_t calibratedPressDuration = const uint16_t calibratedPressDuration =
(calibration < SETTINGS.getPowerButtonDuration()) (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1;
? SETTINGS.getPowerButtonDuration() - calibration
: 1;
inputManager.update(); inputManager.update();
// Verify the user has actually pressed // Verify the user has actually pressed
while (!inputManager.isPressed(InputManager::BTN_POWER) && while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) {
millis() - start < 1000) { delay(10); // only wait 10ms each iteration to not delay too much in case of
delay(10); // only wait 10ms each iteration to not delay too much in case of // short configured duration.
// short configured duration.
inputManager.update(); inputManager.update();
} }
@ -198,8 +179,7 @@ void verifyWakeupLongPress() {
do { do {
delay(10); delay(10);
inputManager.update(); inputManager.update();
} while (inputManager.isPressed(InputManager::BTN_POWER) && } while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < calibratedPressDuration);
inputManager.getHeldTime() < calibratedPressDuration);
abort = inputManager.getHeldTime() < calibratedPressDuration; abort = inputManager.getHeldTime() < calibratedPressDuration;
} else { } else {
abort = true; abort = true;
@ -208,8 +188,7 @@ void verifyWakeupLongPress() {
if (abort) { if (abort) {
// Button released too early. Returning to sleep. // Button released too early. Returning to sleep.
// IMPORTANT: Re-arm the wakeup trigger before sleeping again // IMPORTANT: Re-arm the wakeup trigger before sleeping again
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
ESP_GPIO_WAKEUP_GPIO_LOW);
esp_deep_sleep_start(); esp_deep_sleep_start();
} }
} }
@ -228,11 +207,9 @@ void enterDeepSleep() {
enterNewActivity(new SleepActivity(renderer, mappedInputManager)); enterNewActivity(new SleepActivity(renderer, mappedInputManager));
einkDisplay.deepSleep(); einkDisplay.deepSleep();
Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1);
millis(), t2 - t1);
Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis());
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
ESP_GPIO_WAKEUP_GPIO_LOW);
// Ensure that the power button has been released to avoid immediately turning // Ensure that the power button has been released to avoid immediately turning
// back on if you're holding it // back on if you're holding it
waitForPowerRelease(); waitForPowerRelease();
@ -241,55 +218,43 @@ void enterDeepSleep() {
} }
void onGoHome(); void onGoHome();
void onGoToMyLibraryWithTab(const std::string &path, void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab);
MyLibraryActivity::Tab tab); void onGoToReader(const std::string& initialEpubPath, MyLibraryActivity::Tab fromTab) {
void onGoToReader(const std::string &initialEpubPath,
MyLibraryActivity::Tab fromTab) {
exitActivity(); exitActivity();
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, enterNewActivity(
initialEpubPath, fromTab, onGoHome, new ReaderActivity(renderer, mappedInputManager, initialEpubPath, fromTab, onGoHome, onGoToMyLibraryWithTab));
onGoToMyLibraryWithTab));
}
void onContinueReading() {
onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent);
} }
void onContinueReading() { onGoToReader(APP_STATE.openEpubPath, MyLibraryActivity::Tab::Recent); }
void onGoToFileTransfer() { void onGoToFileTransfer() {
exitActivity(); exitActivity();
enterNewActivity( enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
} }
void onGoToSettings() { void onGoToSettings() {
exitActivity(); exitActivity();
enterNewActivity( enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
new SettingsActivity(renderer, mappedInputManager, onGoHome));
} }
void onGoToMyLibrary() { void onGoToMyLibrary() {
exitActivity(); exitActivity();
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader));
onGoToReader));
} }
void onGoToMyLibraryWithTab(const std::string &path, void onGoToMyLibraryWithTab(const std::string& path, MyLibraryActivity::Tab tab) {
MyLibraryActivity::Tab tab) {
exitActivity(); exitActivity();
enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, tab, path));
onGoToReader, tab, path));
} }
void onGoToBrowser() { void onGoToBrowser() {
exitActivity(); exitActivity();
enterNewActivity( enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome));
new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome));
} }
void onGoHome() { void onGoHome() {
exitActivity(); exitActivity();
enterNewActivity(new HomeActivity( enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings,
renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToFileTransfer, onGoToBrowser));
onGoToSettings, onGoToFileTransfer, onGoToBrowser));
} }
void setupDisplayAndFonts() { void setupDisplayAndFonts() {
@ -309,7 +274,7 @@ void setupDisplayAndFonts() {
renderer.insertFont(OPENDYSLEXIC_10_FONT_ID, opendyslexic10FontFamily); renderer.insertFont(OPENDYSLEXIC_10_FONT_ID, opendyslexic10FontFamily);
renderer.insertFont(OPENDYSLEXIC_12_FONT_ID, opendyslexic12FontFamily); renderer.insertFont(OPENDYSLEXIC_12_FONT_ID, opendyslexic12FontFamily);
renderer.insertFont(OPENDYSLEXIC_14_FONT_ID, opendyslexic14FontFamily); renderer.insertFont(OPENDYSLEXIC_14_FONT_ID, opendyslexic14FontFamily);
#endif // OMIT_FONTS #endif // OMIT_FONTS
renderer.insertFont(UI_10_FONT_ID, ui10FontFamily); renderer.insertFont(UI_10_FONT_ID, ui10FontFamily);
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily); renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
renderer.insertFont(SMALL_FONT_ID, smallFontFamily); renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
@ -325,13 +290,10 @@ bool isWakeupAfterFlashing() {
const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto wakeupCause = esp_sleep_get_wakeup_cause();
const auto resetReason = esp_reset_reason(); const auto resetReason = esp_reset_reason();
return isUsbConnected() && (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && return isUsbConnected() && (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_UNKNOWN);
(resetReason == ESP_RST_UNKNOWN);
} }
bool isSoftwareRestart() { bool isSoftwareRestart() { return esp_reset_reason() == ESP_RST_SW; }
return esp_reset_reason() == ESP_RST_SW;
}
void setup() { void setup() {
t1 = millis(); t1 = millis();
@ -360,8 +322,7 @@ void setup() {
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis()); Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
setupDisplayAndFonts(); setupDisplayAndFonts();
exitActivity(); exitActivity();
enterNewActivity(new FullScreenMessageActivity( enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD));
renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD));
return; return;
} }
@ -375,9 +336,7 @@ void setup() {
// First serial output only here to avoid timing inconsistencies for power // First serial output only here to avoid timing inconsistencies for power
// button press duration verification // button press duration verification
Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION Serial.printf("[%lu] [ ] Starting CrossPoint version " CROSSPOINT_VERSION "\n", millis());
"\n",
millis());
setupDisplayAndFonts(); setupDisplayAndFonts();
@ -417,9 +376,8 @@ void loop() {
inputManager.update(); inputManager.update();
if (Serial && millis() - lastMemPrint >= 10000) { if (Serial && millis() - lastMemPrint >= 10000) {
Serial.printf( Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
"[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", ESP.getHeapSize(), ESP.getMinFreeHeap());
millis(), ESP.getFreeHeap(), ESP.getHeapSize(), ESP.getMinFreeHeap());
lastMemPrint = millis(); lastMemPrint = millis();
} }
@ -428,14 +386,12 @@ void loop() {
static unsigned long lastActivityTime = millis(); static unsigned long lastActivityTime = millis();
if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased() || if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased() ||
(currentActivity && currentActivity->preventAutoSleep())) { (currentActivity && currentActivity->preventAutoSleep())) {
lastActivityTime = millis(); // Reset inactivity timer lastActivityTime = millis(); // Reset inactivity timer
} }
const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs(); const unsigned long sleepTimeoutMs = SETTINGS.getSleepTimeoutMs();
if (millis() - lastActivityTime >= sleepTimeoutMs) { if (millis() - lastActivityTime >= sleepTimeoutMs) {
Serial.printf( Serial.printf("[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n", millis(), sleepTimeoutMs);
"[%lu] [SLP] Auto-sleep triggered after %lu ms of inactivity\n",
millis(), sleepTimeoutMs);
enterDeepSleep(); enterDeepSleep();
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
return; return;
@ -458,9 +414,8 @@ void loop() {
if (loopDuration > maxLoopDuration) { if (loopDuration > maxLoopDuration) {
maxLoopDuration = loopDuration; maxLoopDuration = loopDuration;
if (maxLoopDuration > 50) { if (maxLoopDuration > 50) {
Serial.printf( Serial.printf("[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", millis(), maxLoopDuration,
"[%lu] [LOOP] New max loop duration: %lu ms (activity: %lu ms)\n", activityDuration);
millis(), maxLoopDuration, activityDuration);
} }
} }
@ -468,8 +423,8 @@ void loop() {
// When an activity requests skip loop delay (e.g., webserver running), use // When an activity requests skip loop delay (e.g., webserver running), use
// yield() for faster response Otherwise, use longer delay to save power // yield() for faster response Otherwise, use longer delay to save power
if (currentActivity && currentActivity->skipLoopDelay()) { if (currentActivity && currentActivity->skipLoopDelay()) {
yield(); // Give FreeRTOS a chance to run tasks, but return immediately yield(); // Give FreeRTOS a chance to run tasks, but return immediately
} else { } else {
delay(10); // Normal delay when no activity requires fast response delay(10); // Normal delay when no activity requires fast response
} }
} }