Public release
This commit is contained in:
6
src/Battery.h
Normal file
6
src/Battery.h
Normal file
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <BatteryMonitor.h>
|
||||
|
||||
#define BAT_GPIO0 0 // Battery voltage
|
||||
|
||||
static BatteryMonitor battery(BAT_GPIO0);
|
||||
43
src/Input.cpp
Normal file
43
src/Input.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "Input.h"
|
||||
|
||||
#include <esp32-hal-adc.h>
|
||||
|
||||
void setupInputPinModes() {
|
||||
pinMode(BTN_GPIO1, INPUT);
|
||||
pinMode(BTN_GPIO2, INPUT);
|
||||
pinMode(BTN_GPIO3, INPUT_PULLUP); // Power button
|
||||
}
|
||||
|
||||
// Get currently pressed button by reading ADC values (and digital for power
|
||||
// button)
|
||||
Button getPressedButton() {
|
||||
// Check BTN_GPIO3 (Power button) - digital read
|
||||
if (digitalRead(BTN_GPIO3) == LOW) return POWER;
|
||||
|
||||
// Check BTN_GPIO1 (4 buttons on resistor ladder)
|
||||
const int btn1 = analogRead(BTN_GPIO1);
|
||||
if (btn1 < BTN_RIGHT_VAL + BTN_THRESHOLD) return RIGHT;
|
||||
if (btn1 < BTN_LEFT_VAL + BTN_THRESHOLD) return LEFT;
|
||||
if (btn1 < BTN_CONFIRM_VAL + BTN_THRESHOLD) return CONFIRM;
|
||||
if (btn1 < BTN_BACK_VAL + BTN_THRESHOLD) return BACK;
|
||||
|
||||
// Check BTN_GPIO2 (2 buttons on resistor ladder)
|
||||
const int btn2 = analogRead(BTN_GPIO2);
|
||||
if (btn2 < BTN_VOLUME_DOWN_VAL + BTN_THRESHOLD) return VOLUME_DOWN;
|
||||
if (btn2 < BTN_VOLUME_UP_VAL + BTN_THRESHOLD) return VOLUME_UP;
|
||||
|
||||
return NONE;
|
||||
}
|
||||
|
||||
Input getInput(const bool skipWait) {
|
||||
const Button button = getPressedButton();
|
||||
if (button == NONE) return {NONE, 0};
|
||||
|
||||
if (skipWait) {
|
||||
return {button, 0};
|
||||
}
|
||||
|
||||
const auto start = millis();
|
||||
while (getPressedButton() == button) delay(50);
|
||||
return {button, millis() - start};
|
||||
}
|
||||
28
src/Input.h
Normal file
28
src/Input.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
// 4 buttons on ADC resistor ladder: Back, Confirm, Left, Right
|
||||
#define BTN_GPIO1 1
|
||||
// 2 buttons on ADC resistor ladder: Volume Up, Volume Down
|
||||
#define BTN_GPIO2 2
|
||||
// Power button (digital)
|
||||
#define BTN_GPIO3 3
|
||||
|
||||
// Button ADC thresholds
|
||||
#define BTN_THRESHOLD 100 // Threshold tolerance
|
||||
#define BTN_RIGHT_VAL 3
|
||||
#define BTN_LEFT_VAL 1470
|
||||
#define BTN_CONFIRM_VAL 2655
|
||||
#define BTN_BACK_VAL 3470
|
||||
#define BTN_VOLUME_DOWN_VAL 3
|
||||
#define BTN_VOLUME_UP_VAL 2305
|
||||
|
||||
enum Button { NONE = 0, RIGHT, LEFT, CONFIRM, BACK, VOLUME_UP, VOLUME_DOWN, POWER };
|
||||
|
||||
struct Input {
|
||||
Button button;
|
||||
unsigned long pressTime;
|
||||
};
|
||||
|
||||
void setupInputPinModes();
|
||||
Button getPressedButton();
|
||||
Input getInput(bool skipWait = false);
|
||||
182
src/main.cpp
Normal file
182
src/main.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#include <Arduino.h>
|
||||
#include <EpdRenderer.h>
|
||||
#include <Epub.h>
|
||||
#include <GxEPD2_BW.h>
|
||||
#include <SD.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#include "Battery.h"
|
||||
#include "Input.h"
|
||||
#include "screens/EpubReaderScreen.h"
|
||||
#include "screens/FullScreenMessageScreen.h"
|
||||
|
||||
#define SPI_FQ 40000000
|
||||
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
||||
#define EPD_SCLK 8 // SPI Clock
|
||||
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In)
|
||||
#define EPD_CS 21 // Chip Select
|
||||
#define EPD_DC 4 // Data/Command
|
||||
#define EPD_RST 5 // Reset
|
||||
#define EPD_BUSY 6 // Busy
|
||||
|
||||
#define UART0_RXD 20 // Used for USB connection detection
|
||||
|
||||
#define SD_SPI_CS 12
|
||||
#define SD_SPI_MISO 7
|
||||
|
||||
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
|
||||
EPD_RST, EPD_BUSY));
|
||||
auto renderer = new EpdRenderer(&display);
|
||||
Screen* currentScreen;
|
||||
|
||||
// Power button timing
|
||||
// Time required to confirm boot from sleep
|
||||
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1500;
|
||||
// Time required to enter sleep mode
|
||||
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
|
||||
|
||||
Epub* loadEpub(const std::string& path) {
|
||||
if (!SD.exists(path.c_str())) {
|
||||
Serial.println("File does not exist");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto epub = new Epub(path, "/.crosspoint");
|
||||
if (epub->load()) {
|
||||
return epub;
|
||||
}
|
||||
|
||||
Serial.println("Failed to load epub");
|
||||
free(epub);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void enterNewScreen(Screen* screen) {
|
||||
if (currentScreen) {
|
||||
currentScreen->onExit();
|
||||
delete currentScreen;
|
||||
}
|
||||
currentScreen = screen;
|
||||
currentScreen->onEnter();
|
||||
}
|
||||
|
||||
// Verify long press on wake-up from deep sleep
|
||||
void verifyWakeupLongPress() {
|
||||
const auto input = getInput();
|
||||
|
||||
if (input.button == POWER && input.pressTime > POWER_BUTTON_WAKEUP_MS) {
|
||||
// Button released too early. Returning to sleep.
|
||||
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
}
|
||||
|
||||
// Enter deep sleep mode
|
||||
void enterDeepSleep() {
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Sleeping", true, false, true));
|
||||
|
||||
Serial.println("Power button released after a long press. Entering deep sleep.");
|
||||
delay(2000); // Allow Serial buffer to empty and display to update
|
||||
|
||||
// Enable Wakeup on LOW (button press)
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||
|
||||
display.hibernate();
|
||||
|
||||
// Enter Deep Sleep
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
void setupSerial() {
|
||||
Serial.begin(115200);
|
||||
// Wait for serial monitor
|
||||
const unsigned long start = millis();
|
||||
while (!Serial && (millis() - start) < 3000) {
|
||||
delay(10);
|
||||
}
|
||||
|
||||
if (Serial) {
|
||||
// delay for monitor to start reading
|
||||
delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
setupInputPinModes();
|
||||
|
||||
// Check if boot was triggered by the Power Button (Deep Sleep Wakeup)
|
||||
// If triggered by RST pin or Battery insertion, this will be false, allowing
|
||||
// normal boot.
|
||||
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) {
|
||||
verifyWakeupLongPress();
|
||||
}
|
||||
|
||||
setupSerial();
|
||||
|
||||
// Initialize pins
|
||||
pinMode(BAT_GPIO0, INPUT);
|
||||
|
||||
// Initialize SPI with custom pins
|
||||
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
||||
|
||||
// Initialize display
|
||||
const SPISettings spi_settings(SPI_FQ, MSBFIRST, SPI_MODE0);
|
||||
display.init(115200, true, 2, false, SPI, spi_settings);
|
||||
display.setRotation(3); // 270 degrees
|
||||
display.setTextColor(GxEPD_BLACK);
|
||||
Serial.println("Display initialized");
|
||||
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Loading...", true));
|
||||
|
||||
// SD Card Initialization
|
||||
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
|
||||
|
||||
// TODO: Add a file selection screen, for now just load the first file
|
||||
File root = SD.open("/");
|
||||
String filename;
|
||||
while (true) {
|
||||
filename = root.getNextFileName();
|
||||
if (!filename) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (filename.substring(filename.length() - 5) == ".epub") {
|
||||
Serial.printf("Found epub: %s\n", filename.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!filename) {
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Could not find epub"));
|
||||
return;
|
||||
}
|
||||
|
||||
Epub* epub = loadEpub(std::string(filename.c_str()));
|
||||
if (epub) {
|
||||
enterNewScreen(new EpubReaderScreen(renderer, epub));
|
||||
} else {
|
||||
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub"));
|
||||
}
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(50);
|
||||
|
||||
const Input input = getInput();
|
||||
|
||||
if (input.button == NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.button == POWER && input.pressTime > POWER_BUTTON_SLEEP_MS) {
|
||||
enterDeepSleep();
|
||||
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
||||
delay(1000);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentScreen) {
|
||||
currentScreen->handleInput(input);
|
||||
}
|
||||
}
|
||||
209
src/screens/EpubReaderScreen.cpp
Normal file
209
src/screens/EpubReaderScreen.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "EpubReaderScreen.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include "Battery.h"
|
||||
|
||||
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
||||
|
||||
void EpubReaderScreen::taskTrampoline(void* param) {
|
||||
auto* self = static_cast<EpubReaderScreen*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void EpubReaderScreen::onEnter() {
|
||||
sectionMutex = xSemaphoreCreateMutex();
|
||||
|
||||
epub->setupCacheDir();
|
||||
|
||||
// TODO: Move this to a state object
|
||||
if (SD.exists((epub->getCachePath() + "/progress.bin").c_str())) {
|
||||
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str());
|
||||
uint8_t data[4];
|
||||
f.read(data, 4);
|
||||
currentSpineIndex = data[0] + (data[1] << 8);
|
||||
nextPageNumber = data[2] + (data[3] << 8);
|
||||
Serial.printf("Loaded cache: %d, %d\n", currentSpineIndex, nextPageNumber);
|
||||
f.close();
|
||||
}
|
||||
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
|
||||
xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask",
|
||||
8192, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
}
|
||||
|
||||
void EpubReaderScreen::onExit() {
|
||||
vTaskDelete(displayTaskHandle);
|
||||
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||
vSemaphoreDelete(sectionMutex);
|
||||
sectionMutex = nullptr;
|
||||
}
|
||||
|
||||
void EpubReaderScreen::handleInput(const Input input) {
|
||||
if (input.button == VOLUME_UP || input.button == VOLUME_DOWN) {
|
||||
const bool skipChapter = input.pressTime > SKIP_CHAPTER_MS;
|
||||
|
||||
// No current section, attempt to rerender the book
|
||||
if (!section) {
|
||||
updateRequired = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.button == VOLUME_UP && skipChapter) {
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex--;
|
||||
delete section;
|
||||
section = nullptr;
|
||||
} else if (input.button == VOLUME_DOWN && skipChapter) {
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex++;
|
||||
delete section;
|
||||
section = nullptr;
|
||||
} else if (input.button == VOLUME_UP) {
|
||||
if (section->currentPage > 0) {
|
||||
section->currentPage--;
|
||||
} else {
|
||||
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||
nextPageNumber = UINT16_MAX;
|
||||
currentSpineIndex--;
|
||||
delete section;
|
||||
section = nullptr;
|
||||
xSemaphoreGive(sectionMutex);
|
||||
}
|
||||
} else if (input.button == VOLUME_DOWN) {
|
||||
if (section->currentPage < section->pageCount - 1) {
|
||||
section->currentPage++;
|
||||
} else {
|
||||
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||
nextPageNumber = 0;
|
||||
currentSpineIndex++;
|
||||
delete section;
|
||||
section = nullptr;
|
||||
xSemaphoreGive(sectionMutex);
|
||||
}
|
||||
}
|
||||
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderScreen::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (updateRequired) {
|
||||
updateRequired = false;
|
||||
renderPage();
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderScreen::renderPage() {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentSpineIndex >= epub->getSpineItemsCount() || currentSpineIndex < 0) {
|
||||
currentSpineIndex = 0;
|
||||
}
|
||||
|
||||
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||
if (!section) {
|
||||
const auto filepath = epub->getSpineItem(currentSpineIndex);
|
||||
Serial.printf("Loading file: %s, index: %d\n", filepath.c_str(), currentSpineIndex);
|
||||
section = new Section(epub, currentSpineIndex, renderer);
|
||||
if (!section->hasCache()) {
|
||||
Serial.println("Cache not found, building...");
|
||||
section->setupCacheDir();
|
||||
if (!section->persistPageDataToSD()) {
|
||||
Serial.println("Failed to persist page data to SD");
|
||||
free(section);
|
||||
section = nullptr;
|
||||
xSemaphoreGive(sectionMutex);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Serial.println("Cache found, skipping build...");
|
||||
}
|
||||
|
||||
if (nextPageNumber == UINT16_MAX) {
|
||||
section->currentPage = section->pageCount - 1;
|
||||
} else {
|
||||
section->currentPage = nextPageNumber;
|
||||
}
|
||||
}
|
||||
|
||||
renderer->clearScreen();
|
||||
section->renderPage();
|
||||
renderStatusBar();
|
||||
renderer->flushDisplay();
|
||||
|
||||
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
||||
uint8_t data[4];
|
||||
data[0] = currentSpineIndex & 0xFF;
|
||||
data[1] = (currentSpineIndex >> 8) & 0xFF;
|
||||
data[2] = section->currentPage & 0xFF;
|
||||
data[3] = (section->currentPage >> 8) & 0xFF;
|
||||
f.write(data, 4);
|
||||
f.close();
|
||||
|
||||
xSemaphoreGive(sectionMutex);
|
||||
}
|
||||
|
||||
void EpubReaderScreen::renderStatusBar() const {
|
||||
const auto pageWidth = renderer->getPageWidth();
|
||||
|
||||
std::string progress = std::to_string(currentPage + 1) + " / " + std::to_string(section->pageCount);
|
||||
const auto progressTextWidth = renderer->getSmallTextWidth(progress.c_str());
|
||||
renderer->drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str());
|
||||
|
||||
const uint16_t percentage = battery.readPercentage();
|
||||
auto percentageText = std::to_string(percentage) + "%";
|
||||
const auto percentageTextWidth = renderer->getSmallTextWidth(percentageText.c_str());
|
||||
renderer->drawSmallText(20, 765, percentageText.c_str());
|
||||
|
||||
// 1 column on left, 2 columns on right, 5 columns of battery body
|
||||
constexpr int batteryWidth = 15;
|
||||
constexpr int batteryHeight = 10;
|
||||
const int x = 0;
|
||||
const int y = 772;
|
||||
|
||||
// Top line
|
||||
renderer->drawLine(x, y, x + batteryWidth - 4, y, 1);
|
||||
// Bottom line
|
||||
renderer->drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1, 1);
|
||||
// Left line
|
||||
renderer->drawLine(x, y, x, y + batteryHeight - 1, 1);
|
||||
// Battery end
|
||||
renderer->drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1, 1);
|
||||
renderer->drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 3, y + batteryHeight - 3, 1);
|
||||
renderer->drawLine(x + batteryWidth - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3, 1);
|
||||
renderer->drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3, 1);
|
||||
|
||||
// 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, 1);
|
||||
|
||||
// Page width minus existing content with 30px padding on each side
|
||||
const int leftMargin = 20 + percentageTextWidth + 30;
|
||||
const int rightMargin = progressTextWidth + 30;
|
||||
const int availableTextWidth = pageWidth - leftMargin - rightMargin;
|
||||
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
|
||||
auto title = tocItem.title;
|
||||
int titleWidth = renderer->getSmallTextWidth(title.c_str());
|
||||
while (titleWidth > availableTextWidth) {
|
||||
title = title.substr(0, title.length() - 8) + "...";
|
||||
titleWidth = renderer->getSmallTextWidth(title.c_str());
|
||||
}
|
||||
|
||||
renderer->drawSmallText(leftMargin + (availableTextWidth - titleWidth) / 2, 765, title.c_str());
|
||||
}
|
||||
31
src/screens/EpubReaderScreen.h
Normal file
31
src/screens/EpubReaderScreen.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include <Epub.h>
|
||||
#include <Epub/Section.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include "Screen.h"
|
||||
|
||||
class EpubReaderScreen final : public Screen {
|
||||
Epub* epub;
|
||||
Section* section = nullptr;
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t sectionMutex = nullptr;
|
||||
int currentSpineIndex = 0;
|
||||
int nextPageNumber = 0;
|
||||
int currentPage = 0;
|
||||
bool updateRequired = false;
|
||||
|
||||
static void taskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
void renderPage();
|
||||
void renderStatusBar() const;
|
||||
|
||||
public:
|
||||
explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub) : Screen(renderer), epub(epub) {}
|
||||
~EpubReaderScreen() override { free(section); }
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void handleInput(Input input) override;
|
||||
};
|
||||
14
src/screens/FullScreenMessageScreen.cpp
Normal file
14
src/screens/FullScreenMessageScreen.cpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#include "FullScreenMessageScreen.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
|
||||
void FullScreenMessageScreen::onEnter() {
|
||||
const auto width = renderer->getTextWidth(text.c_str(), bold, italic);
|
||||
const auto height = renderer->getLineHeight();
|
||||
const auto left = (renderer->getPageWidth() - width) / 2;
|
||||
const auto top = (renderer->getPageHeight() - height) / 2;
|
||||
|
||||
renderer->clearScreen(invert);
|
||||
renderer->drawText(left, top, text.c_str(), bold, italic, invert ? 0 : 1);
|
||||
renderer->flushDisplay();
|
||||
}
|
||||
18
src/screens/FullScreenMessageScreen.h
Normal file
18
src/screens/FullScreenMessageScreen.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "Screen.h"
|
||||
|
||||
class FullScreenMessageScreen final : public Screen {
|
||||
std::string text;
|
||||
bool bold;
|
||||
bool italic;
|
||||
bool invert;
|
||||
|
||||
public:
|
||||
explicit FullScreenMessageScreen(EpdRenderer* renderer, std::string text, const bool bold = false,
|
||||
const bool italic = false, const bool invert = false)
|
||||
: Screen(renderer), text(std::move(text)), bold(bold), italic(italic), invert(invert) {}
|
||||
void onEnter() override;
|
||||
};
|
||||
16
src/screens/Screen.h
Normal file
16
src/screens/Screen.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "Input.h"
|
||||
|
||||
class EpdRenderer;
|
||||
|
||||
class Screen {
|
||||
protected:
|
||||
EpdRenderer* renderer;
|
||||
|
||||
public:
|
||||
explicit Screen(EpdRenderer* renderer) : renderer(renderer) {}
|
||||
virtual ~Screen() = default;
|
||||
virtual void onEnter() {}
|
||||
virtual void onExit() {}
|
||||
virtual void handleInput(Input input) {}
|
||||
};
|
||||
Reference in New Issue
Block a user