feat(ui): layout toolkit, Lua launcher and touch press events
Apps describe nesting instead of coordinates: /lib/ui.lua borrows CSS block flow, the box model and auto sizing, and owns hit testing, press capture and the pressed repaint. The runtime gains require backed by the SD card, on_touch_down/on_touch_up, text metrics and rounded gradient fills, so the launcher becomes an ordinary Lua app and the firmware keeps only a fallback screen for an unreadable card.
This commit is contained in:
+26
-63
@@ -13,56 +13,35 @@ static constexpr int SD_MOSI = 23;
|
||||
static constexpr int SD_MISO = 19;
|
||||
static constexpr int TOUCH_CS = 33;
|
||||
|
||||
static const char* LAUNCHER = "/apps/launcher/main.lua";
|
||||
|
||||
TFT_eSPI tft;
|
||||
SPIClass touchSpi(HSPI);
|
||||
SPIClass& sdSpi = SPI;
|
||||
XPT2046_Touchscreen touch(TOUCH_CS);
|
||||
LuaApp app(tft, touch);
|
||||
|
||||
bool sdOk = false;
|
||||
String appPath;
|
||||
bool halted = false;
|
||||
String nextApp;
|
||||
|
||||
void drawLauncher() {
|
||||
// Last resort only: the UI lives in Lua, so this exists purely to explain why
|
||||
// nothing else could run.
|
||||
void fallbackScreen(const char* message) {
|
||||
tft.setRotation(0);
|
||||
tft.fillScreen(TFT_WHITE);
|
||||
tft.setTextColor(TFT_RED, TFT_WHITE);
|
||||
tft.drawString(message, 10, 10);
|
||||
tft.setTextColor(TFT_BLACK, TFT_WHITE);
|
||||
tft.drawString("esp32-lcd - tap an app", 10, 10);
|
||||
|
||||
File dir = SD.open("/apps");
|
||||
int y = 40;
|
||||
while (File entry = dir.openNextFile()) {
|
||||
if (entry.isDirectory() && entry.name()[0] != '.') {
|
||||
tft.drawRect(10, y - 4, tft.width() - 20, 28, TFT_BLACK);
|
||||
tft.drawString(entry.name(), 18, y);
|
||||
y += 36;
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
dir.close();
|
||||
tft.drawString("expected /apps/launcher/main.lua", 10, 30);
|
||||
Serial.printf("halted: %s\n", message);
|
||||
halted = true;
|
||||
}
|
||||
|
||||
// ponytail: first N rows of 36px = first N app dirs, in SD order. A scrollable
|
||||
// menu widget can come later; this is a launcher, not an OS.
|
||||
void launcherTouch() {
|
||||
if (!touch.touched()) return;
|
||||
TS_Point p = touch.getPoint();
|
||||
int16_t tx, ty;
|
||||
LuaApp::mapTouch(tft, p, tx, ty);
|
||||
int idx = (ty - 40) / 36;
|
||||
if (idx < 0) return;
|
||||
|
||||
File dir = SD.open("/apps");
|
||||
int i = 0;
|
||||
while (File entry = dir.openNextFile()) {
|
||||
if (entry.isDirectory() && entry.name()[0] != '.') {
|
||||
if (i++ == idx) {
|
||||
appPath = String("/apps/") + entry.name() + "/main.lua";
|
||||
entry.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
entry.close();
|
||||
}
|
||||
dir.close();
|
||||
void startApp(const String& path) {
|
||||
tft.setRotation(settings.rotationIndex()); // apps may have rotated the frame
|
||||
Serial.printf("launching %s\n", path.c_str());
|
||||
if (app.load(path.c_str())) return;
|
||||
if (path == LAUNCHER) fallbackScreen("launcher failed to start");
|
||||
}
|
||||
|
||||
void setup() {
|
||||
@@ -75,40 +54,24 @@ void setup() {
|
||||
|
||||
sdSpi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
|
||||
if (!SD.begin(SD_CS, sdSpi)) {
|
||||
tft.setTextColor(TFT_RED, TFT_WHITE);
|
||||
tft.drawString("SD card mount failed", 10, 10);
|
||||
Serial.println("SD mount failed");
|
||||
fallbackScreen("SD card mount failed");
|
||||
return;
|
||||
}
|
||||
sdOk = true;
|
||||
settings.load(); // absent file keeps the built-in touch defaults
|
||||
tft.setRotation(settings.rotationIndex());
|
||||
drawLauncher();
|
||||
Serial.println("launcher ready");
|
||||
settings.load(); // absent file keeps the built-in defaults
|
||||
startApp(LAUNCHER);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (!sdOk) return; // SD failed to mount; error already drawn
|
||||
if (halted) return;
|
||||
|
||||
if (app.running()) {
|
||||
app.loop();
|
||||
if (!app.running()) {
|
||||
appPath = "";
|
||||
tft.setRotation(settings.rotationIndex()); // apps may have rotated the frame
|
||||
drawLauncher();
|
||||
Serial.println("launcher ready");
|
||||
}
|
||||
if (!app.running()) nextApp = app.takePendingLaunch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (appPath.length() > 0) {
|
||||
String path = appPath;
|
||||
appPath = "";
|
||||
Serial.printf("launching %s\n", path.c_str());
|
||||
app.load(path.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
launcherTouch();
|
||||
String path = nextApp.length() > 0 ? nextApp : String(LAUNCHER);
|
||||
nextApp = "";
|
||||
startApp(path);
|
||||
delay(10);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user