This commit is contained in:
CaptainFrito 2026-01-19 21:24:15 +07:00 committed by GitHub
commit 27d6ee75bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 626 additions and 144 deletions

View File

@ -131,6 +131,12 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con
} }
} }
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const int lineWidth, const bool state) const {
for (int i = 0; i < lineWidth; i++) {
drawLine(x1, y1 + i, x2, y2 + i, state);
}
}
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const { void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const {
drawLine(x, y, x + width - 1, y, state); drawLine(x, y, x + width - 1, y, state);
drawLine(x + width - 1, y, x + width - 1, y + height - 1, state); drawLine(x + width - 1, y, x + width - 1, y + height - 1, state);
@ -138,6 +144,121 @@ void GfxRenderer::drawRect(const int x, const int y, const int width, const int
drawLine(x, y, x, y + height - 1, state); drawLine(x, y, x, y + height - 1, state);
} }
// Border is inside the rectangle
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const int lineWidth,
const bool state) const {
for (int i = 0; i < lineWidth; i++) {
drawLine(x + i, y + i, x + width - i, y + i, state);
drawLine(x + width - i, y + i, x + width - i, y + height - i, state);
drawLine(x + width - i, y + height - i, x + i, y + height - i, state);
drawLine(x + i, y + height - i, x + i, y + i, state);
}
}
void GfxRenderer::drawArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir,
const int lineWidth, const bool state) const {
const int stroke = std::min(lineWidth, maxRadius);
const int innerRadius = std::max(maxRadius - stroke, 0);
const int outerRadiusSq = maxRadius * maxRadius;
const int innerRadiusSq = innerRadius * innerRadius;
for (int dy = 0; dy <= maxRadius; ++dy) {
for (int dx = 0; dx <= maxRadius; ++dx) {
const int distSq = dx * dx + dy * dy;
if (distSq > outerRadiusSq || distSq < innerRadiusSq) {
continue;
}
const int px = cx + xDir * dx;
const int py = cy + yDir * dy;
drawPixel(px, py, state);
}
}
};
// Use Bayer matrix 4x4 dithering to fill the rectangle with a grey level - 0 white to 15 black
void GfxRenderer::fillRectGrey(const int x, const int y, const int width, const int height, const int greyLevel) const {
static constexpr uint8_t bayer4x4[4][4] = {
{0, 8, 2, 10},
{12, 4, 14, 6},
{3, 11, 1, 9},
{15, 7, 13, 5},
};
static constexpr int matrixSize = 4;
static constexpr int matrixLevels = matrixSize * matrixSize;
const int normalizedGrey = (greyLevel * 255) / (matrixLevels - 1);
const int clampedGrey = std::max(0, std::min(normalizedGrey, 255));
const int threshold = (clampedGrey * (matrixLevels + 1)) / 256;
for (int dy = 0; dy < height; ++dy) {
const int screenY = y + dy;
const int matrixY = screenY & (matrixSize - 1);
for (int dx = 0; dx < width; ++dx) {
const int screenX = x + dx;
const int matrixX = screenX & (matrixSize - 1);
const uint8_t patternValue = bayer4x4[matrixY][matrixX];
const bool black = patternValue < threshold;
drawPixel(screenX, screenY, black);
}
}
}
// Color -1 white, 0 clear, 1 black
void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir,
const int insideColor, const int outsideColor) const {
const int radiusSq = maxRadius * maxRadius;
for (int dy = 0; dy <= maxRadius; ++dy) {
for (int dx = 0; dx <= maxRadius; ++dx) {
const int distSq = dx * dx + dy * dy;
const int px = cx + xDir * dx;
const int py = cy + yDir * dy;
if (distSq > radiusSq) {
if (outsideColor != 0) {
drawPixel(px, py, outsideColor == 1);
}
} else {
if (insideColor != 0) {
drawPixel(px, py, insideColor == 1);
}
}
}
}
};
// Border is inside the rectangle, rounded corners
void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, const int height, const int lineWidth,
const int cornerRadius, const bool state) const {
if (lineWidth <= 0 || width <= 0 || height <= 0) {
return;
}
const int maxRadius = std::min({cornerRadius, width / 2, height / 2});
if (maxRadius <= 0) {
drawRect(x, y, width, height, lineWidth, state);
return;
}
const int stroke = std::min(lineWidth, maxRadius);
const int right = x + width - 1;
const int bottom = y + height - 1;
const int horizontalWidth = width - 2 * maxRadius;
if (horizontalWidth > 0) {
fillRect(x + maxRadius, y, horizontalWidth, stroke, state);
fillRect(x + maxRadius, bottom - stroke + 1, horizontalWidth, stroke, state);
}
const int verticalHeight = height - 2 * maxRadius;
if (verticalHeight > 0) {
fillRect(x, y + maxRadius, stroke, verticalHeight, state);
fillRect(right - stroke + 1, y + maxRadius, stroke, verticalHeight, state);
}
drawArc(maxRadius, x + maxRadius, y + maxRadius, -1, -1, lineWidth, state); // TL
drawArc(maxRadius, right - maxRadius, y + maxRadius, 1, -1, lineWidth, state); // TR
drawArc(maxRadius, right - maxRadius, bottom - maxRadius, 1, 1, lineWidth, state); // BR
drawArc(maxRadius, x + maxRadius, bottom - maxRadius, -1, 1, lineWidth, state); // BL
}
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
for (int fillY = y; fillY < y + height; fillY++) { for (int fillY = y; fillY < y + height; fillY++) {
drawLine(x, fillY, x + width - 1, fillY, state); drawLine(x, fillY, x + width - 1, fillY, state);
@ -152,6 +273,10 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height);
} }
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
einkDisplay.drawImage(bitmap, y, getScreenWidth() - width - x, height, width);
}
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight, void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight,
const float cropX, const float cropY) const { const float cropX, const float cropY) const {
// For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit) // For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit)

View File

@ -63,9 +63,20 @@ class GfxRenderer {
// Drawing // Drawing
void drawPixel(int x, int y, bool state = true) const; void drawPixel(int x, int y, bool state = true) const;
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 drawLine(int x1, int y1, int x2, int y2, const int lineWidth, const bool state) const;
void drawArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir, const int lineWidth,
const bool state) 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 drawRect(const int x, const int y, const int width, const int height, const int lineWidth,
const bool state) const;
void drawRoundedRect(const int x, const int y, const int width, const int height, const int lineWidth,
const int cornerRadius, const bool state) 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;
void fillRectGrey(const int x, const int y, const int width, const int height, const int greyLevel) const;
void fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir, const int insideColor,
const int outsideColor) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const; void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, 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, int maxHeight, float cropX = 0,
float cropY = 0) const; 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;

@ -1 +1 @@
Subproject commit bd4e6707503ab9c97d13ee0d8f8c69e9ff03cd12 Subproject commit fe766f15cb9ff1ef8214210a8667037df4c60b81

View File

@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance;
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; 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 = 20; constexpr uint8_t SETTINGS_COUNT = 21;
constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
@ -49,6 +49,7 @@ bool CrossPointSettings::saveToFile() const {
serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, hideBatteryPercentage);
serialization::writePod(outputFile, longPressChapterSkip); serialization::writePod(outputFile, longPressChapterSkip);
serialization::writePod(outputFile, hyphenationEnabled); serialization::writePod(outputFile, hyphenationEnabled);
serialization::writePod(outputFile, uiTheme);
outputFile.close(); outputFile.close();
Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis());
@ -120,6 +121,8 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, hyphenationEnabled); serialization::readPod(inputFile, hyphenationEnabled);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, uiTheme);
if (++settingsRead >= fileSettingsCount) break;
} while (false); } while (false);
inputFile.close(); inputFile.close();

View File

@ -58,6 +58,9 @@ class CrossPointSettings {
// Hide battery percentage // Hide battery percentage
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 }; enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 };
// UI Theme
enum UI_THEME { CLASSIC = 0, ROUNDED = 1 };
// Sleep screen settings // Sleep screen settings
uint8_t sleepScreen = DARK; uint8_t sleepScreen = DARK;
// Sleep screen cover mode settings // Sleep screen cover mode settings
@ -94,6 +97,8 @@ class CrossPointSettings {
uint8_t hideBatteryPercentage = HIDE_NEVER; uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press chapter skip on side buttons // Long-press chapter skip on side buttons
uint8_t longPressChapterSkip = 1; uint8_t longPressChapterSkip = 1;
// UI Theme
uint8_t uiTheme = CLASSIC;
~CrossPointSettings() = default; ~CrossPointSettings() = default;

View File

@ -1,66 +0,0 @@
#include "ScreenComponents.h"
#include <GfxRenderer.h>
#include <cstdint>
#include <string>
#include "Battery.h"
#include "fontIds.h"
void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top,
const bool showPercentage) {
// Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage();
const auto percentageText = showPercentage ? std::to_string(percentage) + "%" : "";
renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body
constexpr int batteryWidth = 15;
constexpr int batteryHeight = 12;
const int x = left;
const int y = top + 6;
// Top line
renderer.drawLine(x + 1, y, x + batteryWidth - 3, y);
// Bottom line
renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1);
// Left line
renderer.drawLine(x, y + 1, x, y + batteryHeight - 2);
// Battery end
renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2);
renderer.drawPixel(x + batteryWidth - 1, y + 3);
renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4);
renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5);
// The +1 is to round up, so that we always fill at least one pixel
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
if (filledWidth > batteryWidth - 5) {
filledWidth = batteryWidth - 5; // Ensure we don't overflow
}
renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4);
}
void ScreenComponents::drawProgressBar(const GfxRenderer& renderer, const int x, const int y, const int width,
const int height, const size_t current, const size_t total) {
if (total == 0) {
return;
}
// Use 64-bit arithmetic to avoid overflow for large files
const int percent = static_cast<int>((static_cast<uint64_t>(current) * 100) / total);
// Draw outline
renderer.drawRect(x, y, width, height);
// Draw filled portion
const int fillWidth = (width - 4) * percent / 100;
if (fillWidth > 0) {
renderer.fillRect(x + 2, y + 2, fillWidth, height - 4);
}
// Draw percentage text centered below bar
const std::string percentText = std::to_string(percent) + "%";
renderer.drawCenteredText(UI_10_FONT_ID, y + height + 15, percentText.c_str());
}

View File

@ -1,24 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
class GfxRenderer;
class ScreenComponents {
public:
static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true);
/**
* Draw a progress bar with percentage text.
* @param renderer The graphics renderer
* @param x Left position of the bar
* @param y Top position of the bar
* @param width Width of the bar
* @param height Height of the bar
* @param current Current progress value
* @param total Total value for 100% progress
*/
static void drawProgressBar(const GfxRenderer& renderer, int x, int y, int width, int height, size_t current,
size_t total);
};

View File

@ -6,8 +6,8 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "ScreenComponents.h"
#include "activities/network/WifiSelectionActivity.h" #include "activities/network/WifiSelectionActivity.h"
#include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "network/HttpDownloader.h" #include "network/HttpDownloader.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
@ -205,7 +205,7 @@ void OpdsBookBrowserActivity::render() const {
constexpr int barHeight = 20; constexpr int barHeight = 20;
constexpr int barX = 50; constexpr int barX = 50;
const int barY = pageHeight / 2 + 20; const int barY = pageHeight / 2 + 20;
ScreenComponents::drawProgressBar(renderer, barX, barY, barWidth, barHeight, downloadProgress, downloadTotal); UITheme::drawProgressBar(renderer, Rect{barX, barY, barWidth, barHeight}, downloadProgress, downloadTotal);
} }
renderer.displayBuffer(); renderer.displayBuffer();
return; return;

View File

@ -13,7 +13,7 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "ScreenComponents.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
@ -550,7 +550,7 @@ void HomeActivity::render() {
const uint16_t percentage = battery.readPercentage(); const uint16_t percentage = battery.readPercentage();
const auto percentageText = showBatteryPercentage ? std::to_string(percentage) + "%" : ""; const auto percentageText = showBatteryPercentage ? std::to_string(percentage) + "%" : "";
const auto batteryX = pageWidth - 25 - renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); const auto batteryX = pageWidth - 25 - renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
ScreenComponents::drawBattery(renderer, batteryX, 10, showBatteryPercentage); UITheme::drawBattery(renderer, Rect{batteryX, 10}, showBatteryPercentage);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -8,7 +8,7 @@
#include <cstring> #include <cstring>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "ScreenComponents.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
@ -711,7 +711,7 @@ void CalibreWirelessActivity::render() const {
constexpr int barHeight = 20; constexpr int barHeight = 20;
constexpr int barX = 50; constexpr int barX = 50;
const int barY = statusY + 20; const int barY = statusY + 20;
ScreenComponents::drawProgressBar(renderer, barX, barY, barWidth, barHeight, bytesReceived, currentFileSize); UITheme::drawProgressBar(renderer, Rect{barX, barY, barWidth, barHeight}, bytesReceived, currentFileSize);
} }
// Draw error if present // Draw error if present

View File

@ -9,7 +9,7 @@
#include "CrossPointState.h" #include "CrossPointState.h"
#include "EpubReaderChapterSelectionActivity.h" #include "EpubReaderChapterSelectionActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "ScreenComponents.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
@ -455,7 +455,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
} }
if (showBattery) { if (showBattery) {
ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY, showBatteryPercentage); UITheme::drawBattery(renderer, Rect{orientedMarginLeft + 1, textY}, showBatteryPercentage);
} }
if (showChapterTitle) { if (showChapterTitle) {

View File

@ -4,6 +4,7 @@
#include <SDCardManager.h> #include <SDCardManager.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/StringUtils.h" #include "util/StringUtils.h"
@ -180,23 +181,21 @@ void FileSelectionActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Books", true, EpdFontFamily::BOLD);
auto folderName = basepath == "/" ? "SD card" : basepath.substr(basepath.rfind('/') + 1).c_str();
UITheme::drawFullscreenWindowFrame(renderer, folderName);
Rect contentRect = UITheme::getWindowContentFrame(renderer);
// Help text // Help text
const auto labels = mappedInput.mapLabels("« Home", "Open", "", ""); const auto labels = mappedInput.mapLabels("« Home", "Open", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (files.empty()) { if (files.empty()) {
renderer.drawText(UI_10_FONT_ID, 20, 60, "No books found"); renderer.drawText(UI_10_FONT_ID, contentRect.x + 20, contentRect.y + 20, "No books found");
renderer.displayBuffer(); } else {
return; UITheme::drawList(
} renderer, contentRect, files.size(), selectorIndex, [this](int index) { return files[index]; }, false, nullptr,
false, nullptr);
const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS;
renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 - 2, pageWidth - 1, 30);
for (size_t i = pageStartIndex; i < files.size() && i < pageStartIndex + PAGE_ITEMS; i++) {
auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), renderer.getScreenWidth() - 40);
renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, item.c_str(), i != selectorIndex);
} }
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -8,7 +8,7 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "CrossPointState.h" #include "CrossPointState.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "ScreenComponents.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
@ -517,7 +517,7 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int
} }
if (showBattery) { if (showBattery) {
ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY); UITheme::drawBattery(renderer, Rect{orientedMarginLeft, textY});
} }
if (showTitle) { if (showTitle) {

View File

@ -10,11 +10,12 @@
#include "KOReaderSettingsActivity.h" #include "KOReaderSettingsActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "OtaUpdateActivity.h" #include "OtaUpdateActivity.h"
#include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
// Define the static settings list // Define the static settings list
namespace { namespace {
constexpr int settingsCount = 22; constexpr int settingsCount = 23;
const SettingInfo settingsList[settingsCount] = { const SettingInfo settingsList[settingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}),
@ -27,7 +28,7 @@ const SettingInfo settingsList[settingsCount] = {
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout, SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}), {"Bck, OK, L, R", "L, R, Bck, OK", "L, Bck, OK, R"}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}), {"Prev, Next", "Next, Prev"}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip),
@ -43,6 +44,7 @@ const SettingInfo settingsList[settingsCount] = {
{"1 min", "5 min", "10 min", "15 min", "30 min"}), {"1 min", "5 min", "10 min", "15 min", "30 min"}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}), {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}),
SettingInfo::Enum("UI Theme", &CrossPointSettings::uiTheme, {"Classic", "Rounded"}),
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("KOReader Sync"),
SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Calibre Settings"),
SettingInfo::Action("Check for updates")}; SettingInfo::Action("Check for updates")};
@ -82,6 +84,8 @@ void SettingsActivity::onExit() {
} }
vSemaphoreDelete(renderingMutex); vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr; renderingMutex = nullptr;
UITheme::initialize(); // Re-apply theme in case it was changed
} }
void SettingsActivity::loop() { void SettingsActivity::loop() {
@ -193,36 +197,26 @@ void SettingsActivity::render() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header UITheme::drawFullscreenWindowFrame(renderer, "Settings");
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD); Rect contentRect = UITheme::getWindowContentFrame(renderer);
// Draw selection highlight UITheme::drawList(
renderer.fillRect(0, 60 + selectedSettingIndex * 30 - 2, pageWidth - 1, 30); renderer, contentRect, settingsCount, selectedSettingIndex,
[](int index) { return std::string(settingsList[index].name); }, false, nullptr, true,
// Draw all settings [this](int i) {
for (int i = 0; i < settingsCount; i++) { const auto& setting = settingsList[i];
const int settingY = 60 + i * 30; // 30 pixels between settings std::string valueText = "";
const bool isSelected = (i == selectedSettingIndex); if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr);
// Draw setting name valueText = value ? "ON" : "OFF";
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
// Draw value based on setting type valueText = settingsList[i].enumValues[value];
std::string valueText; } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
const bool value = SETTINGS.*(settingsList[i].valuePtr); }
valueText = value ? "ON" : "OFF"; return valueText;
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { });
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
valueText = settingsList[i].enumValues[value];
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
}
if (!valueText.empty()) {
const auto width = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, valueText.c_str(), !isSelected);
}
}
// Draw version text above button hints // Draw version text above button hints
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),

View File

@ -0,0 +1,70 @@
#include "UITheme.h"
#include <GfxRenderer.h>
#include <memory>
#include "components/themes/BaseTheme.h"
#include "components/themes/rounded/RoundedTheme.h"
std::unique_ptr<BaseTheme> currentTheme = nullptr;
// Initialize theme based on settings
void UITheme::initialize() {
auto themeType = static_cast<CrossPointSettings::UI_THEME>(SETTINGS.uiTheme);
setTheme(themeType);
}
void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
switch (type) {
case CrossPointSettings::UI_THEME::CLASSIC:
Serial.printf("[%lu] [UI] Using Classic theme\n", millis());
currentTheme = std::unique_ptr<BaseTheme>(new BaseTheme());
break;
case CrossPointSettings::UI_THEME::ROUNDED:
Serial.printf("[%lu] [UI] Using Rounded theme\n", millis());
currentTheme = std::unique_ptr<BaseTheme>(new RoundedTheme());
break;
}
}
// Forward all component methods to the current theme
void UITheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) {
if (currentTheme != nullptr) {
currentTheme->drawProgressBar(renderer, rect, current, total);
}
}
void UITheme::drawBattery(const GfxRenderer& renderer, Rect rect, bool showPercentage) {
if (currentTheme != nullptr) {
currentTheme->drawBattery(renderer, rect, showPercentage);
}
}
void UITheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
const std::function<std::string(int index)>& rowIcon, bool hasValue,
const std::function<std::string(int index)>& rowValue) {
if (currentTheme != nullptr) {
currentTheme->drawList(renderer, rect, itemCount, selectedIndex, rowTitle, hasIcon, rowIcon, hasValue, rowValue);
}
}
void UITheme::drawWindowFrame(GfxRenderer& renderer, Rect rect, bool isPopup, const char* title) {
if (currentTheme != nullptr) {
currentTheme->drawWindowFrame(renderer, rect, isPopup, title);
}
}
void UITheme::drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title) {
if (currentTheme != nullptr) {
currentTheme->drawFullscreenWindowFrame(renderer, title);
}
}
Rect UITheme::getWindowContentFrame(GfxRenderer& renderer) {
if (currentTheme != nullptr) {
return currentTheme->getWindowContentFrame(renderer);
}
return Rect{};
}

35
src/components/UITheme.h Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include <functional>
#include "CrossPointSettings.h"
class GfxRenderer;
struct Rect {
int x;
int y;
int width;
int height;
// Constructor for explicit initialization
explicit Rect(int x = 0, int y = 0, int width = 0, int height = 0) : x(x), y(y), width(width), height(height) {}
};
class UITheme {
public:
static void initialize();
static void setTheme(CrossPointSettings::UI_THEME type);
static Rect getWindowContentFrame(GfxRenderer& renderer);
static void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total);
static void drawBattery(const GfxRenderer& renderer, Rect rect, bool showPercentage = true);
static void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
const std::function<std::string(int index)>& rowIcon, bool hasValue,
const std::function<std::string(int index)>& rowValue);
static void drawWindowFrame(GfxRenderer& renderer, Rect rect, bool isPopup, const char* title);
static void drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title);
};

View File

@ -0,0 +1,111 @@
#include "BaseTheme.h"
#include <GfxRenderer.h>
#include <cstdint>
#include <string>
#include "Battery.h"
#include "fontIds.h"
namespace {
constexpr int rowHeight = 30;
constexpr int pageItems = 23;
} // namespace
void BaseTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) {
// Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage();
const auto percentageText = showPercentage ? std::to_string(percentage) + "%" : "";
renderer.drawText(SMALL_FONT_ID, rect.x + 20, rect.y, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body
constexpr int batteryWidth = 15;
constexpr int batteryHeight = 12;
const int x = rect.x;
const int y = rect.y + 6;
// Top line
renderer.drawLine(x + 1, y, x + batteryWidth - 3, y);
// Bottom line
renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1);
// Left line
renderer.drawLine(x, y + 1, x, y + batteryHeight - 2);
// Battery end
renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2);
renderer.drawPixel(x + batteryWidth - 1, y + 3);
renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4);
renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5);
// The +1 is to round up, so that we always fill at least one pixel
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
if (filledWidth > batteryWidth - 5) {
filledWidth = batteryWidth - 5; // Ensure we don't overflow
}
renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4);
}
void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const size_t current, const size_t total) {
if (total == 0) {
return;
}
// Use 64-bit arithmetic to avoid overflow for large files
const int percent = static_cast<int>((static_cast<uint64_t>(current) * 100) / total);
// Draw outline
renderer.drawRect(rect.x, rect.y, rect.width, rect.height);
// Draw filled portion
const int fillWidth = (rect.width - 4) * percent / 100;
if (fillWidth > 0) {
renderer.fillRect(rect.x + 2, rect.y + 2, fillWidth, rect.height - 4);
}
// Draw percentage text centered below bar
const std::string percentText = std::to_string(percent) + "%";
renderer.drawCenteredText(UI_10_FONT_ID, rect.y + rect.height + 15, percentText.c_str());
}
void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
const std::function<std::string(int index)>& rowIcon, bool hasValue,
const std::function<std::string(int index)>& rowValue) {
// Draw selection
renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width - 1, rowHeight);
// Draw all items
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
const int itemY = rect.y + (i % pageItems) * rowHeight;
// Draw name
auto itemName = rowTitle(i);
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(),
rect.width - (hasValue ? 100 : 40)); // TODO truncate according to value width?
renderer.drawText(UI_10_FONT_ID, 20, itemY, item.c_str(), i != selectedIndex);
if (hasValue) {
// Draw value
std::string valueText = rowValue(i);
const auto textWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, rect.width - 20 - textWidth, itemY, valueText.c_str(), i != selectedIndex);
}
}
}
Rect BaseTheme::getWindowContentFrame(GfxRenderer& renderer) {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
return Rect{0, 60, pageWidth, pageHeight - 120};
}
void BaseTheme::drawWindowFrame(GfxRenderer& renderer, Rect rect, bool isPopup, const char* title) {
if (title) {
renderer.drawCenteredText(UI_12_FONT_ID, rect.y, title, true, EpdFontFamily::BOLD);
}
}
void BaseTheme::drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title) {
BaseTheme::drawWindowFrame(renderer, Rect{0, 15, renderer.getScreenWidth(), renderer.getScreenHeight()}, false,
title);
}

View File

@ -0,0 +1,31 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <functional>
#include "components/UITheme.h"
class GfxRenderer;
// Default theme implementation (Classic Theme)
// Additional themes can inherit from this and override methods as needed
class BaseTheme {
public:
virtual ~BaseTheme() = default;
// Property getters
virtual Rect getWindowContentFrame(GfxRenderer& renderer);
// Component drawing methods
virtual void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total);
virtual void drawBattery(const GfxRenderer& renderer, Rect rect, bool showPercentage = true);
virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
const std::function<std::string(int index)>& rowIcon, bool hasValue,
const std::function<std::string(int index)>& rowValue);
virtual void drawWindowFrame(GfxRenderer& renderer, Rect rect, bool isPopup, const char* title);
virtual void drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title);
};

View File

@ -0,0 +1,160 @@
#include "RoundedTheme.h"
#include <GfxRenderer.h>
#include <cstdint>
#include <string>
#include "Battery.h"
#include "fontIds.h"
namespace {
constexpr int rowHeight = 64;
constexpr int pageItems = 9;
constexpr int windowCornerRadius = 16;
constexpr int windowBorderWidth = 2;
constexpr int fullscreenWindowMargin = 20;
constexpr int windowHeaderHeight = 50;
constexpr int statusBarHeight = 50;
constexpr int buttonHintsHeight = 50;
} // namespace
void RoundedTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) {
// Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage();
const auto percentageText = showPercentage ? std::to_string(percentage) + "%" : "";
renderer.drawText(SMALL_FONT_ID, rect.x + 20, rect.y, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body
constexpr int batteryWidth = 15;
constexpr int batteryHeight = 12;
const int x = rect.x;
const int y = rect.y + 6;
// Top line
renderer.drawLine(x + 1, y, x + batteryWidth - 3, y);
// Bottom line
renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1);
// Left line
renderer.drawLine(x, y + 1, x, y + batteryHeight - 2);
// Battery end
renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2);
renderer.drawPixel(x + batteryWidth - 1, y + 3);
renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4);
renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5);
// Draw bars
if (percentage > 10) {
renderer.fillRect(x + 2, y + 2, 3, batteryHeight - 4);
}
if (percentage > 40) {
renderer.fillRect(x + 6, y + 2, 3, batteryHeight - 4);
}
if (percentage > 70) {
renderer.fillRect(x + 10, y + 2, 2, batteryHeight - 4);
}
}
void RoundedTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const size_t current, const size_t total) {
if (total == 0) {
return;
}
// Use 64-bit arithmetic to avoid overflow for large files
const int percent = static_cast<int>((static_cast<uint64_t>(current) * 100) / total);
// Draw filled portion
const int fillWidth = (rect.width - 4) * percent / 100;
if (fillWidth > 0) {
renderer.fillRect(rect.x + 2, rect.y + 2, fillWidth, rect.height - 4);
}
// Draw percentage text centered below bar
const std::string percentText = std::to_string(percent) + "%";
renderer.drawCenteredText(UI_10_FONT_ID, rect.y + rect.height + 15, percentText.c_str());
}
void RoundedTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
const std::function<std::string(int index)>& rowIcon, bool hasValue,
const std::function<std::string(int index)>& rowValue) {
const int totalPages = (itemCount + pageItems - 1) / pageItems;
if (totalPages > 1) {
// Draw scroll bar
const int scrollBarHeight = (rect.height * pageItems) / itemCount;
const int currentPage = selectedIndex / pageItems;
const int scrollBarY = rect.y + ((rect.height - scrollBarHeight) * currentPage) / (totalPages - 1);
renderer.fillRectGrey(rect.x + rect.width, rect.y, 4, rect.height, 5);
renderer.fillRect(rect.x + rect.width, scrollBarY, 4, scrollBarHeight, true);
}
// Draw selection
renderer.fillRectGrey(rect.x, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width - 1, rowHeight, 3);
// Draw all items
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
const int itemY = rect.y + (i % pageItems) * rowHeight;
// Draw name
auto itemName = rowTitle(i);
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(),
rect.width - (hasValue ? 100 : 40)); // TODO truncate according to value width?
renderer.drawText(UI_10_FONT_ID, rect.x + 20, itemY + 20, item.c_str(), true);
if (hasValue) {
// Draw value
std::string valueText = rowValue(i);
const auto textWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
renderer.drawText(UI_10_FONT_ID, rect.x + rect.width - 20 - textWidth, itemY + 20, valueText.c_str(), true);
}
}
}
Rect RoundedTheme::getWindowContentFrame(GfxRenderer& renderer) {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
return Rect{35, 125, pageWidth - 70, pageHeight - 165 - buttonHintsHeight};
}
void RoundedTheme::drawWindowFrame(GfxRenderer& renderer, Rect rect, bool isPopup, const char* title) {
const int windowWidth = renderer.getScreenWidth() - 2 * rect.x;
if (title) { // Header background
renderer.fillRectGrey(rect.x, rect.y, windowWidth, windowHeaderHeight, 5);
renderer.fillArc(windowCornerRadius, rect.x + windowCornerRadius, rect.y + windowCornerRadius, -1, -1, 0,
-1); // TL
renderer.fillArc(windowCornerRadius, windowWidth + rect.x - windowCornerRadius, rect.y + windowCornerRadius, 1, -1,
0, -1); // TR
}
renderer.drawRoundedRect(rect.x, rect.y, windowWidth, rect.height, windowBorderWidth, windowCornerRadius, true);
if (!isPopup) {
renderer.drawLine(windowWidth + rect.x, rect.y + windowCornerRadius + 2, windowWidth + rect.x,
rect.y + rect.height - windowCornerRadius, windowBorderWidth, true);
renderer.drawLine(rect.x + windowCornerRadius + 2, rect.y + rect.height, windowWidth + rect.x - windowCornerRadius,
rect.y + rect.height, windowBorderWidth, true);
renderer.drawArc(windowCornerRadius + windowBorderWidth, windowWidth + rect.x - 1 - windowCornerRadius,
rect.y + rect.height - 1 - windowCornerRadius, 1, 1, windowBorderWidth, true);
renderer.drawPixel(rect.x + windowCornerRadius + 1, rect.y + rect.height, true);
}
if (title) { // Header
const int titleWidth = renderer.getTextWidth(UI_12_FONT_ID, title);
const int titleX = (renderer.getScreenWidth() - titleWidth) / 2;
const int titleY = rect.y + 10;
renderer.drawText(UI_12_FONT_ID, titleX, titleY, title, true, EpdFontFamily::REGULAR);
renderer.drawLine(rect.x, rect.y + windowHeaderHeight, windowWidth + rect.x, rect.y + windowHeaderHeight,
windowBorderWidth, true);
}
}
void RoundedTheme::drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title) {
// drawStatusBar(renderer);
RoundedTheme::drawWindowFrame(
renderer,
Rect{fullscreenWindowMargin, statusBarHeight, 0,
renderer.getScreenHeight() - fullscreenWindowMargin - statusBarHeight - buttonHintsHeight},
false, title);
RoundedTheme::drawBattery(renderer, Rect{fullscreenWindowMargin, 18}, false);
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "components/themes/BaseTheme.h"
class GfxRenderer;
class RoundedTheme : public BaseTheme {
public:
// Property getters
Rect getWindowContentFrame(GfxRenderer& renderer) override;
// Component drawing methods
void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override;
void drawBattery(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) override;
void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle, bool hasIcon,
const std::function<std::string(int index)>& rowIcon, bool hasValue,
const std::function<std::string(int index)>& rowValue) override;
void drawWindowFrame(GfxRenderer& renderer, Rect rect, bool isPopup, const char* title) override;
void drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title) override;
};

View File

@ -22,6 +22,7 @@
#include "activities/reader/ReaderActivity.h" #include "activities/reader/ReaderActivity.h"
#include "activities/settings/SettingsActivity.h" #include "activities/settings/SettingsActivity.h"
#include "activities/util/FullScreenMessageActivity.h" #include "activities/util/FullScreenMessageActivity.h"
#include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#define SPI_FQ 40000000 #define SPI_FQ 40000000
@ -273,6 +274,7 @@ void setup() {
} }
inputManager.begin(); inputManager.begin();
// Initialize pins // Initialize pins
pinMode(BAT_GPIO0, INPUT); pinMode(BAT_GPIO0, INPUT);
@ -291,6 +293,7 @@ void setup() {
SETTINGS.loadFromFile(); SETTINGS.loadFromFile();
KOREADER_STORE.loadFromFile(); KOREADER_STORE.loadFromFile();
UITheme::initialize();
// verify power button press duration after we've read settings. // verify power button press duration after we've read settings.
verifyWakeupLongPress(); verifyWakeupLongPress();