Grid Browser with file thumbnails

This commit is contained in:
CaptainFrito 2025-12-27 16:10:39 -08:00
parent 838246d147
commit 06ce25f0cd
10 changed files with 633 additions and 2 deletions

View File

@ -80,6 +80,74 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
}
}
void GfxRenderer::drawTextInBox(const int fontId, const int x, const int y, const int w, const int h, const char* text, const bool centered, const bool black, const EpdFontStyle style) const {
const int lineHeight = getLineHeight(fontId);
const int spaceWidth = getSpaceWidth(fontId);
int xpos = x;
int ypos = y + lineHeight;
if (centered) {
int textWidth = getTextWidth(fontId, text, style);
if (textWidth < w) {
// Center if text on single line
xpos = x + (w - textWidth) / 2;
}
}
// cannot draw a NULL / empty string
if (text == nullptr || *text == '\0') {
return;
}
if (fontMap.count(fontId) == 0) {
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
return;
}
const auto font = fontMap.at(fontId);
// no printable characters
if (!font.hasPrintableChars(text, style)) {
return;
}
uint32_t cp;
int ellipsisWidth = 0;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
const int charWidth = getTextWidth(fontId, reinterpret_cast<const char*>(&cp), style);
if (xpos + charWidth + ellipsisWidth > x + w) {
if (ellipsisWidth > 0) {
// Draw ellipsis and exit
int dotX = xpos;
renderChar(font, '.', &dotX, &ypos, black, style);
dotX += spaceWidth/3;
renderChar(font, '.', &dotX, &ypos, black, style);
dotX += spaceWidth/3;
renderChar(font, '.', &dotX, &ypos, black, style);
break;
} else {
// TODO center when more than one line
// if (centered) {
// int textWidth = getTextWidth(fontId, text, style);
// if (textWidth < w) {
// xpos = x + (w - textWidth) / 2;
// }
// }
xpos = x;
ypos += lineHeight;
if (h > 0 && ypos - y > h) {
// Overflowing box height
break;
}
if (h > 0 && ypos + lineHeight - y > h) {
// Last line, prepare ellipsis
ellipsisWidth = spaceWidth * 4;
}
}
}
renderChar(font, cp, &xpos, &ypos, black, style);
}
}
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) const {
if (x1 == x2) {
if (y2 < y1) {
@ -101,6 +169,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 {
drawLine(x, y, x + width - 1, y, state);
drawLine(x + width - 1, y, x + width - 1, y + height - 1, state);
@ -108,6 +182,68 @@ void GfxRenderer::drawRect(const int x, const int y, const int width, const int
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);
}
}
};
// 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 {
for (int fillY = y; fillY < y + height; fillY++) {
drawLine(x, fillY, x + width - 1, fillY, state);
@ -119,6 +255,10 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co
einkDisplay.drawImage(bitmap, y, x, height, width);
}
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) const {
float scale = 1.0f;

View File

@ -45,15 +45,21 @@ class GfxRenderer {
// Drawing
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, int lineWidth, 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, int lineWidth, bool state) const;
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool state) const;
void fillRect(int x, int y, int width, int height, bool state = true) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawIcon(const uint8_t bitmap[], int x, int y, int width, int height) const;
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
// Text
int getTextWidth(int fontId, const char* text, EpdFontStyle style = REGULAR) const;
void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
void drawText(int fontId, int x, int y, const char* text, bool black = true, EpdFontStyle style = REGULAR) const;
void drawTextInBox(int fontId, int x, int y, int w, int h, const char* text, bool centered, bool black = true, EpdFontStyle style = REGULAR) const;
int getSpaceWidth(int fontId) const;
int getLineHeight(int fontId) const;

View File

@ -29,6 +29,9 @@ class CrossPointSettings {
uint8_t extraParagraphSpacing = 1;
// Duration of the power button press
uint8_t shortPwrBtn = 0;
// UI Theme
enum UI_THEME { LIST = 0, GRID = 1 };
uint8_t uiTheme = GRID;
~CrossPointSettings() = default;

View File

@ -0,0 +1,279 @@
#include "GridBrowserActivity.h"
#include <GfxRenderer.h>
#include <SD.h>
#include <InputManager.h>
#include "config.h"
#include "../../images/FolderIcon.h"
#include "../util/Window.h"
namespace {
constexpr int PAGE_ITEMS = 9;
constexpr int SKIP_PAGE_MS = 700;
constexpr int TILE_W = 135;
constexpr int TILE_H = 200;
constexpr int TILE_PADDING = 5;
constexpr int THUMB_W = 90;
constexpr int THUMB_H = 120;
constexpr int TILE_TEXT_H = 60;
constexpr int gridLeftOffset = 45;
constexpr int gridTopOffset = 125;
} // namespace
inline int min(const int a, const int b) { return a < b ? a : b; }
void GridBrowserActivity::sortFileList(std::vector<FileInfo>& strs) {
std::sort(begin(strs), end(strs), [](const FileInfo& f1, const FileInfo& f2) {
if (f1.type == F_DIRECTORY && f2.type != F_DIRECTORY) return true;
if (f1.type != F_DIRECTORY && f2.type == F_DIRECTORY) return false;
return lexicographical_compare(
begin(f1.name), end(f1.name), begin(f2.name), end(f2.name),
[](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); });
});
}
void GridBrowserActivity::taskTrampoline(void* param) {
auto* self = static_cast<GridBrowserActivity*>(param);
self->displayTaskLoop();
}
void GridBrowserActivity::loadFiles() {
files.clear();
selectorIndex = 0;
previousSelectorIndex = -1;
page = 0;
auto root = SD.open(basepath.c_str());
for (File file = root.openNextFile(); file; file = root.openNextFile()) {
const std::string filename = std::string(file.name());
if (filename.empty() || filename[0] == '.') {
file.close();
continue;
}
if (file.isDirectory()) {
files.emplace_back(FileInfo{ filename, filename, F_DIRECTORY });
} else {
FileType type = F_FILE;
size_t dot = filename.find_first_of('.');
std::string basename = filename;
if (dot != std::string::npos) {
std::string ext = filename.substr(dot);
basename = filename.substr(0, dot);
// lowercase ext for case-insensitive compare
for (char &c : ext) c = (char)tolower(c);
if (ext == ".epub") {
type = F_EPUB;
} else if (ext == ".thumb.bmp") {
type = F_BMP;
}
}
if (type != F_FILE) {
files.emplace_back(FileInfo{ filename, basename, type });
}
}
file.close();
}
root.close();
Serial.printf("Files loaded\n");
GridBrowserActivity::sortFileList(files);
Serial.printf("Files sorted\n");
}
void GridBrowserActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex();
loadFiles();
selectorIndex = 0;
page = 0;
// Trigger first render
renderRequired = true;
xTaskCreate(&GridBrowserActivity::taskTrampoline, "GridFileBrowserTask",
8192, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
}
void GridBrowserActivity::onExit() {
Activity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
files.clear();
}
void GridBrowserActivity::loop() {
const bool prevReleased = inputManager.wasReleased(InputManager::BTN_UP) || inputManager.wasReleased(InputManager::BTN_LEFT);
const bool nextReleased = inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT);
const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS;
const int selected = selectorIndex + page * PAGE_ITEMS;
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
if (files.empty()) {
return;
}
if (basepath.back() != '/') {
basepath += "/";
}
if (files[selected].type == F_DIRECTORY) {
// open subfolder
basepath += files[selected].name;
loadFiles();
renderRequired = true;
} else {
onSelect(basepath + files[selected].name);
}
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
if (basepath != "/") {
basepath = basepath.substr(0, basepath.rfind('/'));
if (basepath.empty()) basepath = "/";
loadFiles();
renderRequired = true;
} else {
// At root level, go back home
onGoHome();
}
} else if (prevReleased) {
previousSelectorIndex = selectorIndex;
if (selectorIndex == 0 || skipPage) {
if (page > 0) {
page--;
selectorIndex = 0;
previousSelectorIndex = -1;
renderRequired = true;
}
} else {
selectorIndex--;
updateRequired = true;
}
} else if (nextReleased) {
previousSelectorIndex = selectorIndex;
if (selectorIndex == min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS) - 1 || skipPage) {
if (page < files.size() / PAGE_ITEMS) {
page++;
selectorIndex = 0;
previousSelectorIndex = -1;
renderRequired = true;
}
} else {
selectorIndex++;
updateRequired = true;
}
}
}
void GridBrowserActivity::displayTaskLoop() {
while (true) {
if (renderRequired) {
renderRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render(true);
xSemaphoreGive(renderingMutex);
} else if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// update(true);
render(false);
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void GridBrowserActivity::render(bool clear) const {
if (clear) {
renderer.clearScreen();
auto folderName = basepath == "/" ? "SD card" : basepath.substr(basepath.rfind('/') + 1).c_str();
drawFullscreenWindowFrame(renderer, folderName);
}
bool hasGeyscaleBitmaps = false;
if (!files.empty()) {
for (int pass = 0; pass < 3; pass++) {
if (pass > 0) {
renderer.clearScreen(0x00);
if (pass == 1) {
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
} else if (pass == 2) {
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
}
}
for (size_t i = 0; i < min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS); i++) {
const auto file = files[i + page * PAGE_ITEMS];
const int16_t tileX = gridLeftOffset + i % 3 * TILE_W;
const int16_t tileY = gridTopOffset + i / 3 * TILE_H;
if (pass == 0) {
if (file.type == F_DIRECTORY) {
constexpr int iconOffsetX = (TILE_W - FOLDERICON_WIDTH) / 2;
constexpr int iconOffsetY = (TILE_H - TILE_TEXT_H - FOLDERICON_HEIGHT) / 2;
renderer.drawIcon(FolderIcon, tileX + iconOffsetX, tileY + iconOffsetY, FOLDERICON_WIDTH, FOLDERICON_HEIGHT);
}
}
if (file.type == F_BMP) {
File bmpFile = SD.open((basepath + "/" + file.name).c_str());
if (bmpFile) {
Bitmap bitmap(bmpFile);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
if (bitmap.hasGreyscale()) {
hasGeyscaleBitmaps = true;
}
constexpr int thumbOffsetX = (TILE_W - THUMB_W) / 2;
constexpr int thumbOffsetY = (TILE_H - TILE_TEXT_H - THUMB_H) / 2;
renderer.drawBitmap(bitmap, tileX + thumbOffsetX, tileY + thumbOffsetY, THUMB_W, THUMB_H);
}
}
}
if (pass == 0) {
renderer.drawTextInBox(UI_FONT_ID, tileX + TILE_PADDING, tileY + TILE_H - TILE_TEXT_H, TILE_W - 2 * TILE_PADDING, TILE_TEXT_H, file.basename.c_str(), true);
}
}
if (pass == 0) {
update(false);
renderer.displayBuffer();
if (hasGeyscaleBitmaps) {
renderer.storeBwBuffer();
} else {
// we can skip grayscale passes if no bitmaps use it
break;
}
} else if (pass == 1) {
renderer.copyGrayscaleLsbBuffers();
} else if (pass == 2) {
renderer.copyGrayscaleMsbBuffers();
renderer.displayGrayBuffer();
renderer.setRenderMode(GfxRenderer::BW);
renderer.restoreBwBuffer();
}
}
}
}
void GridBrowserActivity::drawSelectionRectangle(int tileIndex, bool black) const {
renderer.drawRoundedRect(gridLeftOffset + tileIndex % 3 * TILE_W, gridTopOffset + tileIndex / 3 * TILE_H, TILE_W, TILE_H, 2, 5, black);
}
void GridBrowserActivity::update(bool render) const {
// Redraw only changed tiles
// renderer.clearScreen();
if (previousSelectorIndex >= 0) {
drawSelectionRectangle(previousSelectorIndex, false);
}
drawSelectionRectangle(selectorIndex, true);
}

View File

@ -0,0 +1,60 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
#include <vector>
#include "../Activity.h"
enum FileType {
F_DIRECTORY = 0,
F_EPUB,
F_TXT,
F_BMP,
F_FILE
};
struct FileInfo {
std::string name;
std::string basename;
FileType type;
};
class GridBrowserActivity final : public Activity {
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
std::string basepath = "/";
std::vector<FileInfo> files;
int selectorIndex = 0;
int previousSelectorIndex = -1;
int page;
bool updateRequired = false;
bool renderRequired = false;
const std::function<void(const std::string&)> onSelect;
const std::function<void()> onGoHome;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render(bool clear) const;
void update(bool render) const;
void loadFiles();
void drawSelectionRectangle(int tileIndex, bool black) const;
public:
explicit GridBrowserActivity(GfxRenderer& renderer, InputManager& inputManager,
const std::function<void(const std::string&)>& onSelect,
const std::function<void()>& onGoHome,
std::string initialPath = "/")
: Activity("FileSelection", renderer, inputManager),
onSelect(onSelect),
onGoHome(onGoHome),
basepath(initialPath.empty() ? "/" : std::move(initialPath)) {}
void onEnter() override;
void onExit() override;
void loop() override;
private:
static void sortFileList(std::vector<FileInfo>& strs);
};

View File

@ -5,7 +5,9 @@
#include "Epub.h"
#include "EpubReaderActivity.h"
#include "FileSelectionActivity.h"
#include "../home/GridBrowserActivity.h"
#include "activities/util/FullScreenMessageActivity.h"
#include "../../CrossPointSettings.h"
std::string ReaderActivity::extractFolderPath(const std::string& filePath) {
const auto lastSlash = filePath.find_last_of('/');
@ -51,8 +53,13 @@ void ReaderActivity::onGoToFileSelection(const std::string& fromEpubPath) {
exitActivity();
// If coming from a book, start in that book's folder; otherwise start from root
const auto initialPath = fromEpubPath.empty() ? "/" : extractFolderPath(fromEpubPath);
enterNewActivity(new FileSelectionActivity(
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack, initialPath));
if (SETTINGS.uiTheme == CrossPointSettings::GRID) {
enterNewActivity(new GridBrowserActivity(
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack, initialPath));
} else {
enterNewActivity(new FileSelectionActivity(
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack, initialPath));
}
}
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {

View File

@ -16,6 +16,7 @@ const SettingInfo settingsList[settingsCount] = {
{"Status Bar", SettingType::ENUM, &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}},
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}},
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}},
{"UI Theme", SettingType::ENUM, &CrossPointSettings::uiTheme, {"List", "Grid"}},
{"Check for updates", SettingType::ACTION, nullptr, {}},
};
} // namespace

View File

@ -0,0 +1,71 @@
#include "./Window.h"
#include "Battery.h"
#include "config.h"
namespace {
constexpr int windowCornerRadius = 16;
constexpr int windowBorderWidth = 2;
constexpr int fullscreenWindowMargin = 20;
constexpr int windowHeaderHeight = 50;
constexpr int statusBarHeight = 50;
constexpr int batteryWidth = 15;
constexpr int batteryHeight = 10;
} // namespace
void drawWindowFrame(GfxRenderer& renderer, int xMargin, int y, int height, bool hasShadow, const char* title) {
const int windowWidth = GfxRenderer::getScreenWidth() - 2 * xMargin;
renderer.drawRoundedRect(xMargin, y, windowWidth, height, windowBorderWidth, windowCornerRadius, true);
if (hasShadow) {
renderer.drawLine(windowWidth + xMargin, y + windowCornerRadius + 2, windowWidth + xMargin, y + height - windowCornerRadius, windowBorderWidth, true);
renderer.drawLine(xMargin + windowCornerRadius + 2, y + height, windowWidth + xMargin - windowCornerRadius, y + height, windowBorderWidth, true);
renderer.drawArc(windowCornerRadius + windowBorderWidth, windowWidth + xMargin - 1 - windowCornerRadius, y + height - 1 - windowCornerRadius, 1, 1, windowBorderWidth, true);
renderer.drawPixel(xMargin + windowCornerRadius + 1, y + height, true);
}
if (title) { // Header
const int titleWidth = renderer.getTextWidth(UI_FONT_ID, title);
const int titleX = (GfxRenderer::getScreenWidth() - titleWidth) / 2;
const int titleY = y + 10;
renderer.drawText(UI_FONT_ID, titleX, titleY, title, true, REGULAR);
renderer.drawLine(xMargin, y + windowHeaderHeight, windowWidth + xMargin, y + windowHeaderHeight, windowBorderWidth, true);
}
}
void drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title) {
drawStatusBar(renderer);
drawWindowFrame(renderer, fullscreenWindowMargin, statusBarHeight, GfxRenderer::getScreenHeight() - fullscreenWindowMargin - statusBarHeight, true, title);
}
void drawStatusBar(GfxRenderer& renderer) {
constexpr auto textY = 18;
// Left aligned battery icon and percentage
const uint16_t percentage = battery.readPercentage();
const auto percentageText = std::to_string(percentage) + "%";
const auto percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
renderer.drawText(SMALL_FONT_ID, fullscreenWindowMargin + batteryWidth + 5, textY, percentageText.c_str());
// 1 column on left, 2 columns on right, 5 columns of battery body
constexpr int x = fullscreenWindowMargin;
constexpr int y = textY + 5;
// Top line
renderer.drawLine(x, y, x + batteryWidth - 4, y);
// Bottom line
renderer.drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1);
// Left line
renderer.drawLine(x, y, x, y + batteryHeight - 1);
// Battery end
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
renderer.drawLine(x + batteryWidth - 3, y + batteryHeight - 3, x + batteryWidth - 1, y + batteryHeight - 3);
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
// 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 + 1, y + 1, filledWidth, batteryHeight - 2);
}

View File

@ -0,0 +1,6 @@
#pragma once
#include <GfxRenderer.h>
void drawWindowFrame(GfxRenderer& renderer, int xMargin, int y, int height, bool hasShadow, const char* title);
void drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title);
void drawStatusBar(GfxRenderer& renderer);

58
src/images/FolderIcon.h Normal file
View File

@ -0,0 +1,58 @@
#pragma once
#include <cstdint>
#define FOLDERICON_WIDTH 80
#define FOLDERICON_HEIGHT 80
static const uint8_t FolderIcon[] = {
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0x00,
0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFE, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF,
0xFF, 0xFF, 0xF8, 0xFF, 0x3F, 0xFF, 0xFF, 0xFE, 0x01, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF, 0x3F, 0xFF,
0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xF3, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF,
0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF,
0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF,
0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF,
0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF,
0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF,
0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF,
0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF,
0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xE7, 0xFF, 0x3F, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0xCF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF,
0x8F, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0x1F, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0xFE, 0x1F, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xF8, 0x7F, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF,
0xFF, 0xE3, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF, 0x3F, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xCF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F,
0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF,
0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F,
0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF,
0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF,
0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF,
0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x9F,
0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x81, 0xFF, 0xFF, 0x9F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF,
0x81, 0xFF, 0xFF, 0x8F, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x83, 0xFF, 0xFF, 0xC7, 0xFF, 0xFF,
0x3F, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFE, 0x0F, 0xFF,
0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};