New Apps - Lot 1 (#27)
* New Apps - Lot 1 Trying to mix it up a little with some fun, useful and just weird. Adds more new apps... Brainfuck Breakout Magic 8 Ball Todo List Also re-organised the app name part of main.yml * Fix build? * fixes * and some more * Update Brainfuck.cpp * i heard you like fixes with your fixes * no hard coded paths
This commit is contained in:
@@ -12,7 +12,7 @@ jobs:
|
|||||||
Build:
|
Build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
app_name: [Calculator, Diceware, GPIO, GraphicsDemo, HelloWorld, SerialConsole, TwoEleven, TamaTac, MystifyDemo, Snake]
|
app_name: [Brainfuck, Breakout, Calculator, Diceware, GPIO, GraphicsDemo, HelloWorld, Magic8Ball, MystifyDemo, SerialConsole, Snake, TamaTac, TodoList, TwoEleven]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(Brainfuck)
|
||||||
|
tactility_project(Brainfuck)
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Brainfuck
|
||||||
|
|
||||||
|
A Brainfuck esoteric programming language interpreter for Tactility.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Run Brainfuck programs on your device. Includes built-in examples and support for loading custom scripts. Features cycle limit protection to prevent infinite loops from locking up the device.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Full Brainfuck VM with 4096-byte tape
|
||||||
|
- Cycle limit protection (2,000,000 cycles max)
|
||||||
|
- Built-in examples (Hello World, Fibonacci, Alphabet, Beer song)
|
||||||
|
- Multi-line code editor
|
||||||
|
- Execution statistics (cycle count display)
|
||||||
|
- Load custom scripts from user data directory
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- **Run Button**: Execute Brainfuck code
|
||||||
|
- **Enter Key**: Run code from input area
|
||||||
|
- **Toolbar Buttons**:
|
||||||
|
- Trash icon: Clear output and input
|
||||||
|
- List icon: Toggle between examples/scripts list and editor
|
||||||
|
|
||||||
|
## Custom Scripts
|
||||||
|
|
||||||
|
Place `.bf` or `.b` files (up to 32 KB each) in the app's user data directory to load them from the app. The directory path is shown in the script list when no scripts are found.
|
||||||
|
|
||||||
|
## Brainfuck Reference
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `>` | Move pointer right |
|
||||||
|
| `<` | Move pointer left |
|
||||||
|
| `+` | Increment byte at pointer |
|
||||||
|
| `-` | Decrement byte at pointer |
|
||||||
|
| `.` | Output byte as ASCII |
|
||||||
|
| `,` | Input byte (not supported) |
|
||||||
|
| `[` | Jump forward past `]` if byte is zero |
|
||||||
|
| `]` | Jump back to `[` if byte is nonzero |
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Tactility
|
||||||
|
- Touchscreen or keyboard
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES
|
||||||
|
Source/*.c*
|
||||||
|
)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
# Library headers must be included directly,
|
||||||
|
# because all regular dependencies get stripped by elf_loader's cmake script
|
||||||
|
INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
#include "Brainfuck.h"
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
/* ── Built-in examples ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
struct BfExample {
|
||||||
|
const char* name;
|
||||||
|
const char* code;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const BfExample examples[] = {
|
||||||
|
{
|
||||||
|
"Hello World (built-in)",
|
||||||
|
"++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]"
|
||||||
|
">>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Fibonacci (built-in)",
|
||||||
|
">++++++++++>+>+[[+++++[>++++++++<-]>.<++++++[>--------<-]+<<<"
|
||||||
|
"]>.>>[[-]<[>+<-]>>[<<+>+>-]<[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-"
|
||||||
|
"[>+<-[>+<-[>+<-[>[-]>+>+<<<-[>+<-]]]]]]]]]]]+>>>]<<<]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Alphabet (built-in)",
|
||||||
|
"+++++[>+++++++++++++<-]++++++++++++++++++++++++++[>.+<-]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Beer (built-in)",
|
||||||
|
">++++++++++[<++++++++++>-]<->>>>>+++[>+++>+++<<-]<<<<+<[>[>+"
|
||||||
|
">+<<-]>>[-<<+>>]++++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]>>"
|
||||||
|
">>>>[[-]<++++++++++<->>]<-[>+>+<<-]>[<+>-]+>[[-]<->]<<<<<<<<"
|
||||||
|
"<->>]<[>+>+<<-]>>[-<<+>>]+>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<"
|
||||||
|
"<[>>+>+<<<-]>>>[-<<<+>>>]++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<"
|
||||||
|
"<[>+<[-]]<[>>+<<[-]]>>[<<+>>[-]]<<<[>>+>+<<<-]>>>[-<<<+>>>]+"
|
||||||
|
"+++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[>+<[-]]<[>>+<<[-]]>>[<"
|
||||||
|
"<+>>[-]]<<[[-]>>>++++++++[>>++++++<<-]>[<++++++++[>++++++<-]"
|
||||||
|
">.<++++++++[>------<-]>[<<+>>-]]>.<<++++++++[>>------<<-]<[-"
|
||||||
|
">>+<<]<++++++++[<++++>-]<.>+++++++[>+++++++++<-]>+++.<+++++["
|
||||||
|
">+++++++++<-]>.+++++..--------.-------.++++++++++++++>>[>>>+"
|
||||||
|
">+<<<<-]>>>>[-<<<<+>>>>]>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<<<"
|
||||||
|
"[>>>+>+<<<<-]>>>>[-<<<<+>>>>]+>+<[-<->]<[[-]>>-<<]>>[[-]<<+>"
|
||||||
|
">]<<<[>>+<<[-]]>[>+<[-]]++>>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<"
|
||||||
|
"+<[[-]>-<]>[<<<<<<<.>>>>>>>[-]]<<<<<<<<<.>>----.---------.<<"
|
||||||
|
".>>----.+++..+++++++++++++.[-]<<[-]]<[>+>+<<-]>>[-<<+>>]+>+<"
|
||||||
|
"[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<<[>>+>+<<<-]>>>[-<<<+>>>]++++"
|
||||||
|
">+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[>+<[-]]<[>>+<<[-]]>>[<<+>"
|
||||||
|
">[-]]<<[[-]>++++++++[<++++>-]<.>++++++++++[>+++++++++++<-]>+"
|
||||||
|
".-.<<.>>++++++.------------.---.<<.>++++++[>+++<-]>.<++++++["
|
||||||
|
">----<-]>++.+++++++++++..[-]<<[-]++++++++++.[-]]<[>+>+<<-]>>"
|
||||||
|
"[-<<+>>]+++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]++++++++++."
|
||||||
|
">+++++++++[>+++++++++<-]>+++.+++++++++++++.++++++++++.------"
|
||||||
|
".<++++++++[>>++++<<-]>>.<++++++++++.-.---------.>.<-.+++++++"
|
||||||
|
"++++.++++++++.---------.>.<-------------.+++++++++++++.-----"
|
||||||
|
"-----.>.<++++++++++++.---------------.<+++[>++++++<-]>..>.<-"
|
||||||
|
"---------.+++++++++++.>.<<+++[>------<-]>-.+++++++++++++++++"
|
||||||
|
".---.++++++.-------.----------.[-]>[-]<<<.[-]]<[>+>+<<-]>>[-"
|
||||||
|
"<<+>>]++++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]++++++++++.["
|
||||||
|
"-]<[-]>]<+<]"
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int NUM_EXAMPLES = sizeof(examples) / sizeof(examples[0]);
|
||||||
|
|
||||||
|
/* ── App handle for user data path ────────────────────────────────── */
|
||||||
|
|
||||||
|
static AppHandle s_appHandle = nullptr;
|
||||||
|
|
||||||
|
static bool getScriptDir(char* buf, size_t bufSize) {
|
||||||
|
if (!s_appHandle) return false;
|
||||||
|
size_t size = bufSize;
|
||||||
|
tt_app_get_user_data_path(s_appHandle, buf, &size);
|
||||||
|
if (size == 0) return false;
|
||||||
|
mkdir(buf, 0755);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char** scriptPaths = nullptr;
|
||||||
|
static int scriptCount = 0;
|
||||||
|
|
||||||
|
static int ciStrcmp(const char* a, const char* b) {
|
||||||
|
while (*a && *b) {
|
||||||
|
int ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
|
||||||
|
int cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
|
||||||
|
if (ca != cb) return ca - cb;
|
||||||
|
a++; b++;
|
||||||
|
}
|
||||||
|
return (unsigned char)*a - (unsigned char)*b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void freeScriptPaths() {
|
||||||
|
for (int i = 0; i < scriptCount; i++) {
|
||||||
|
free(scriptPaths[i]);
|
||||||
|
}
|
||||||
|
free(scriptPaths);
|
||||||
|
scriptPaths = nullptr;
|
||||||
|
scriptCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File-scope instance pointer for index-based and path-based callbacks */
|
||||||
|
static Brainfuck* g_instance = nullptr;
|
||||||
|
|
||||||
|
/* ── VM Logic ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void Brainfuck::bfInit() {
|
||||||
|
memset(&vm, 0, sizeof(BfVM));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int bfFindBracket(const char* code, int pos, int dir) {
|
||||||
|
int depth = 1;
|
||||||
|
while (depth > 0) {
|
||||||
|
pos += dir;
|
||||||
|
if (pos < 0 || code[pos] == '\0') return -1;
|
||||||
|
if (code[pos] == '[') depth += dir;
|
||||||
|
if (code[pos] == ']') depth -= dir;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::bfRun(const char* code) {
|
||||||
|
int len = strlen(code);
|
||||||
|
|
||||||
|
while (vm.pc < len && !vm.error) {
|
||||||
|
vm.cycles++;
|
||||||
|
if (vm.cycles > MAX_CYCLES) {
|
||||||
|
vm.error = true;
|
||||||
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Halted after %d cycles (infinite loop?)", MAX_CYCLES);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char c = code[vm.pc];
|
||||||
|
switch (c) {
|
||||||
|
case '>':
|
||||||
|
vm.ptr++;
|
||||||
|
if (vm.ptr >= TAPE_SIZE) {
|
||||||
|
vm.error = true;
|
||||||
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Pointer overflow (>%d)", TAPE_SIZE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '<':
|
||||||
|
vm.ptr--;
|
||||||
|
if (vm.ptr < 0) {
|
||||||
|
vm.error = true;
|
||||||
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Pointer underflow (<0)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '+': vm.tape[vm.ptr]++; break;
|
||||||
|
case '-': vm.tape[vm.ptr]--; break;
|
||||||
|
case '.':
|
||||||
|
if (vm.outLen < MAX_OUTPUT - 1) {
|
||||||
|
vm.output[vm.outLen++] = (char)vm.tape[vm.ptr];
|
||||||
|
vm.output[vm.outLen] = '\0';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ',': vm.tape[vm.ptr] = 0; break;
|
||||||
|
case '[':
|
||||||
|
if (vm.tape[vm.ptr] == 0) {
|
||||||
|
int target = bfFindBracket(code, vm.pc, 1);
|
||||||
|
if (target < 0) {
|
||||||
|
vm.error = true;
|
||||||
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Unmatched '[' at pos %d", vm.pc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vm.pc = target;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ']':
|
||||||
|
if (vm.tape[vm.ptr] != 0) {
|
||||||
|
int target = bfFindBracket(code, vm.pc, -1);
|
||||||
|
if (target < 0) {
|
||||||
|
vm.error = true;
|
||||||
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Unmatched ']' at pos %d", vm.pc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
vm.pc = target;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
vm.pc++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::runCode(const char* code) {
|
||||||
|
if (!outputTa) return;
|
||||||
|
bfInit();
|
||||||
|
bfRun(code);
|
||||||
|
|
||||||
|
constexpr int resultSize = MAX_OUTPUT + 128;
|
||||||
|
char* result = (char*)malloc(resultSize);
|
||||||
|
if (!result) {
|
||||||
|
lv_textarea_set_text(outputTa, "Out of memory");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int pos = 0;
|
||||||
|
int remaining;
|
||||||
|
|
||||||
|
remaining = resultSize - pos;
|
||||||
|
if (remaining > 0) {
|
||||||
|
int n = snprintf(result + pos, remaining, "> RUN (%d chars)\n", (int)strlen(code));
|
||||||
|
pos += (n < remaining) ? n : (remaining - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vm.outLen > 0) {
|
||||||
|
remaining = resultSize - pos;
|
||||||
|
if (remaining > 0) {
|
||||||
|
int n = snprintf(result + pos, remaining, "%s\n", vm.output);
|
||||||
|
pos += (n < remaining) ? n : (remaining - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vm.error) {
|
||||||
|
remaining = resultSize - pos;
|
||||||
|
if (remaining > 0) {
|
||||||
|
int n = snprintf(result + pos, remaining, "ERROR: %s\n", vm.errorMsg);
|
||||||
|
pos += (n < remaining) ? n : (remaining - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remaining = resultSize - pos;
|
||||||
|
if (remaining > 0) {
|
||||||
|
int n = snprintf(result + pos, remaining, "OK (%d cycles)\n", vm.cycles);
|
||||||
|
pos += (n < remaining) ? n : (remaining - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_textarea_set_text(outputTa, result);
|
||||||
|
lv_obj_scroll_to_y(outputTa, LV_COORD_MAX, LV_ANIM_ON);
|
||||||
|
free(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── View management ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void Brainfuck::showMainView() {
|
||||||
|
state = BfState::Main;
|
||||||
|
if (examplesList) lv_obj_add_flag(examplesList, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (outputTa) lv_obj_remove_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (inputRow) lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (clrBtn) lv_obj_remove_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::showExamplesView() {
|
||||||
|
state = BfState::Examples;
|
||||||
|
if (outputTa) lv_obj_add_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (inputRow) lv_obj_add_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (examplesList) lv_obj_remove_flag(examplesList, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (clrBtn) lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Callbacks ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void Brainfuck::onRunClicked(lv_event_t* e) {
|
||||||
|
if (!g_instance || !g_instance->inputTa) return;
|
||||||
|
const char* code = lv_textarea_get_text(g_instance->inputTa);
|
||||||
|
if (code && code[0]) {
|
||||||
|
g_instance->runCode(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::onClearClicked(lv_event_t* e) {
|
||||||
|
if (!g_instance) return;
|
||||||
|
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "");
|
||||||
|
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::onExamplesClicked(lv_event_t* e) {
|
||||||
|
if (!g_instance) return;
|
||||||
|
if (g_instance->state == BfState::Examples) {
|
||||||
|
g_instance->showMainView();
|
||||||
|
} else {
|
||||||
|
g_instance->showExamplesView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::onExampleSelected(lv_event_t* e) {
|
||||||
|
if (!g_instance) return;
|
||||||
|
int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||||
|
if (idx >= 0 && idx < NUM_EXAMPLES) {
|
||||||
|
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, examples[idx].code);
|
||||||
|
g_instance->showMainView();
|
||||||
|
g_instance->runCode(examples[idx].code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::onFileSelected(lv_event_t* e) {
|
||||||
|
if (!g_instance) return;
|
||||||
|
const char* path = (const char*)lv_event_get_user_data(e);
|
||||||
|
FILE* f = fopen(path, "rb");
|
||||||
|
if (!f) {
|
||||||
|
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "Cannot open file");
|
||||||
|
g_instance->showMainView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long fsize = ftell(f);
|
||||||
|
if (fsize <= 0 || fsize > 32768) {
|
||||||
|
fclose(f);
|
||||||
|
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "File too large or empty");
|
||||||
|
g_instance->showMainView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
|
||||||
|
char* buf = (char*)malloc(fsize + 1);
|
||||||
|
if (!buf) {
|
||||||
|
fclose(f);
|
||||||
|
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "Out of memory");
|
||||||
|
g_instance->showMainView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t bytesRead = fread(buf, 1, fsize, f);
|
||||||
|
fclose(f);
|
||||||
|
buf[bytesRead] = '\0';
|
||||||
|
|
||||||
|
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, buf);
|
||||||
|
g_instance->showMainView();
|
||||||
|
g_instance->runCode(buf);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::onInputReady(lv_event_t* e) {
|
||||||
|
onRunClicked(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Script list building ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void Brainfuck::buildScriptList(lv_obj_t* list) {
|
||||||
|
freeScriptPaths();
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_EXAMPLES; i++) {
|
||||||
|
lv_obj_t* btn = lv_list_add_button(list, LV_SYMBOL_PLAY, examples[i].name);
|
||||||
|
lv_obj_add_event_cb(btn, onExampleSelected, LV_EVENT_CLICKED, (void*)(intptr_t)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
char scriptDir[256];
|
||||||
|
if (!getScriptDir(scriptDir, sizeof(scriptDir))) {
|
||||||
|
lv_list_add_text(list, "No storage available for .bf files");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DIR* dir = opendir(scriptDir);
|
||||||
|
if (!dir) {
|
||||||
|
char hint[300];
|
||||||
|
snprintf(hint, sizeof(hint), "Put .bf files in %s", scriptDir);
|
||||||
|
lv_list_add_text(list, hint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dirent* entry;
|
||||||
|
while ((entry = readdir(dir)) != nullptr) {
|
||||||
|
const char* name = entry->d_name;
|
||||||
|
size_t len = strlen(name);
|
||||||
|
if (len < 3) continue;
|
||||||
|
|
||||||
|
const char* ext3 = &name[len - 3];
|
||||||
|
const char* ext2 = &name[len - 2];
|
||||||
|
if (ciStrcmp(ext3, ".bf") != 0 && ciStrcmp(ext2, ".b") != 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t pathLen = strlen(scriptDir) + 1 + len + 1;
|
||||||
|
char* path = (char*)malloc(pathLen);
|
||||||
|
if (!path) break;
|
||||||
|
snprintf(path, pathLen, "%s/%s", scriptDir, name);
|
||||||
|
|
||||||
|
char** tmp = (char**)realloc(scriptPaths, sizeof(char*) * (scriptCount + 1));
|
||||||
|
if (!tmp) { free(path); break; }
|
||||||
|
scriptPaths = tmp;
|
||||||
|
scriptPaths[scriptCount] = path;
|
||||||
|
|
||||||
|
lv_obj_t* btn = lv_list_add_button(list, LV_SYMBOL_FILE, name);
|
||||||
|
lv_obj_add_event_cb(btn, onFileSelected, LV_EVENT_CLICKED, path);
|
||||||
|
scriptCount++;
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
|
||||||
|
if (scriptCount == 0) {
|
||||||
|
lv_list_add_text(list, "No custom scripts found on storage");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Lifecycle ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void Brainfuck::onShow(AppHandle app, lv_obj_t* parent) {
|
||||||
|
g_instance = this;
|
||||||
|
s_appHandle = app;
|
||||||
|
state = BfState::Examples;
|
||||||
|
|
||||||
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||||
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
clrBtn = tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, nullptr);
|
||||||
|
lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, nullptr);
|
||||||
|
|
||||||
|
lv_obj_t* cont = lv_obj_create(parent);
|
||||||
|
lv_obj_set_width(cont, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(cont, 1);
|
||||||
|
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(cont, 4, 0);
|
||||||
|
lv_obj_set_style_pad_gap(cont, 4, 0);
|
||||||
|
lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_style_border_width(cont, 0, 0);
|
||||||
|
|
||||||
|
outputTa = lv_textarea_create(cont);
|
||||||
|
lv_textarea_set_text(outputTa, "");
|
||||||
|
lv_textarea_set_cursor_click_pos(outputTa, false);
|
||||||
|
lv_obj_add_state(outputTa, LV_STATE_DISABLED);
|
||||||
|
lv_obj_set_width(outputTa, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(outputTa, 1);
|
||||||
|
lv_obj_set_style_text_font(outputTa, lv_font_get_default(), 0);
|
||||||
|
lv_obj_add_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
|
inputRow = lv_obj_create(cont);
|
||||||
|
lv_obj_set_size(inputRow, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(inputRow, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(inputRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_all(inputRow, 0, 0);
|
||||||
|
lv_obj_set_style_pad_gap(inputRow, 4, 0);
|
||||||
|
lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_style_border_width(inputRow, 0, 0);
|
||||||
|
lv_obj_add_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
|
inputTa = lv_textarea_create(inputRow);
|
||||||
|
lv_textarea_set_placeholder_text(inputTa, "++++++[>++++++++++<-]>+++++.");
|
||||||
|
lv_textarea_set_one_line(inputTa, false);
|
||||||
|
lv_obj_set_flex_grow(inputTa, 1);
|
||||||
|
lv_obj_set_height(inputTa, 50);
|
||||||
|
lv_obj_set_style_text_font(inputTa, lv_font_get_default(), 0);
|
||||||
|
lv_obj_add_event_cb(inputTa, onInputReady, LV_EVENT_READY, nullptr);
|
||||||
|
|
||||||
|
lv_obj_t* runBtn = lv_button_create(inputRow);
|
||||||
|
lv_obj_t* runLbl = lv_label_create(runBtn);
|
||||||
|
lv_label_set_text(runLbl, LV_SYMBOL_PLAY);
|
||||||
|
lv_obj_add_event_cb(runBtn, onRunClicked, LV_EVENT_CLICKED, nullptr);
|
||||||
|
|
||||||
|
examplesList = lv_list_create(cont);
|
||||||
|
lv_obj_set_width(examplesList, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(examplesList, 1);
|
||||||
|
buildScriptList(examplesList);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Brainfuck::onHide(AppHandle app) {
|
||||||
|
freeScriptPaths();
|
||||||
|
outputTa = nullptr;
|
||||||
|
inputTa = nullptr;
|
||||||
|
inputRow = nullptr;
|
||||||
|
examplesList = nullptr;
|
||||||
|
clrBtn = nullptr;
|
||||||
|
g_instance = nullptr;
|
||||||
|
s_appHandle = nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
constexpr int TAPE_SIZE = 4096;
|
||||||
|
constexpr int MAX_OUTPUT = 2048;
|
||||||
|
constexpr int MAX_CYCLES = 2000000;
|
||||||
|
|
||||||
|
struct BfVM {
|
||||||
|
uint8_t tape[TAPE_SIZE];
|
||||||
|
int ptr;
|
||||||
|
int pc;
|
||||||
|
int cycles;
|
||||||
|
char output[MAX_OUTPUT];
|
||||||
|
int outLen;
|
||||||
|
bool error;
|
||||||
|
char errorMsg[64];
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class BfState {
|
||||||
|
Main,
|
||||||
|
Examples,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Brainfuck final : public App {
|
||||||
|
|
||||||
|
private:
|
||||||
|
// UI pointers (nulled in onHide)
|
||||||
|
lv_obj_t* outputTa = nullptr;
|
||||||
|
lv_obj_t* inputTa = nullptr;
|
||||||
|
lv_obj_t* inputRow = nullptr;
|
||||||
|
lv_obj_t* examplesList = nullptr;
|
||||||
|
lv_obj_t* clrBtn = nullptr;
|
||||||
|
|
||||||
|
BfState state = BfState::Examples;
|
||||||
|
BfVM vm = {};
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
void bfInit();
|
||||||
|
void bfRun(const char* code);
|
||||||
|
void runCode(const char* code);
|
||||||
|
void buildScriptList(lv_obj_t* list);
|
||||||
|
|
||||||
|
// View management
|
||||||
|
void showMainView();
|
||||||
|
void showExamplesView();
|
||||||
|
|
||||||
|
// Static callbacks
|
||||||
|
static void onRunClicked(lv_event_t* e);
|
||||||
|
static void onClearClicked(lv_event_t* e);
|
||||||
|
static void onExamplesClicked(lv_event_t* e);
|
||||||
|
static void onExampleSelected(lv_event_t* e);
|
||||||
|
static void onFileSelected(lv_event_t* e);
|
||||||
|
static void onInputReady(lv_event_t* e);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void onShow(AppHandle context, lv_obj_t* parent) override;
|
||||||
|
void onHide(AppHandle context) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include "Brainfuck.h"
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
registerApp<Brainfuck>();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.7.0-dev
|
||||||
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
|
[app]
|
||||||
|
id=one.tactility.brainfuck
|
||||||
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=Brainfuck esoteric language interpreter
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(Breakout)
|
||||||
|
tactility_project(Breakout)
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Breakout
|
||||||
|
|
||||||
|
A classic brick-breaking arcade game inspired by Arkanoid, built for Tactility devices. Features power-up capsules, multi-hit bricks, multi-ball action, and responsive scaling across all screen sizes.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **8 brick colors** with per-color scoring (50-120 points)
|
||||||
|
- **Silver bricks** that take multiple hits to break (level 3+)
|
||||||
|
- **Gold bricks** that are indestructible (level 5+)
|
||||||
|
- **7 power-up capsules** that drop from destroyed bricks
|
||||||
|
- **12 handcrafted level patterns** + seeded procedural generation (level 13+)
|
||||||
|
- **High score** persistence across sessions
|
||||||
|
- **Sound toggle** with persistent setting
|
||||||
|
- **Responsive scaling** for all Tactility devices (Cardputer to 800x480+ panels)
|
||||||
|
- Touch, Trackball and Keyboard input support
|
||||||
|
|
||||||
|
## Levels
|
||||||
|
|
||||||
|
Each level has a unique brick layout pattern. Colors rotate each level, and Silver/Gold bricks are added as difficulty increases.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Level 1: Full Grid Level 2: Checkerboard Level 3: Diamond
|
||||||
|
[][][][][][][][] [] [] [] [] [] [][][][][][]
|
||||||
|
[][][][][][][][] [] [] [] [] [] [][][][]
|
||||||
|
[][][][][][][][] [] [] [] [] [] [][][][][][]
|
||||||
|
[][][][][][][][] [] [] [] [] [] [][][][]
|
||||||
|
|
||||||
|
Level 4: Stripes Level 5: Pyramid Level 6: Inverted V
|
||||||
|
[][][][][][][][] [][][][][][][][] []
|
||||||
|
. [][][][][] . [][][][]
|
||||||
|
[][][][][][][][] . [][][] . [][][][][][]
|
||||||
|
. [] . [][][][][][][][]
|
||||||
|
|
||||||
|
Level 7: Vert Stripes Level 8: Zigzag Level 9: Blocks
|
||||||
|
[] [] [] [] [] [][] . [][] . [][] . [][] .
|
||||||
|
[] [] [] [] [] . [][] . [][] [][] . [][] .
|
||||||
|
[] [] [] [] [] [][] . [][] . . [][] . [][]
|
||||||
|
[] [] [] [] [] . [][] . [][] . [][] . [][]
|
||||||
|
|
||||||
|
Level 10: Double Diamond Level 11: Border Frame Level 12: Center Cross
|
||||||
|
[][] [][] [][][][][][][][] [] []
|
||||||
|
[][][] [][][] [][][][][][]
|
||||||
|
[][] [][] [][][][][][][][] [] []
|
||||||
|
[][][] [][][] [][][][][][]
|
||||||
|
```
|
||||||
|
|
||||||
|
After level 12, layouts are **procedurally generated** using a seeded algorithm — the same level number always produces the same layout.
|
||||||
|
|
||||||
|
## Power-Up Capsules
|
||||||
|
|
||||||
|
Capsules drop from destroyed bricks (~15% chance) and fall toward the paddle. Catch them to activate!
|
||||||
|
|
||||||
|
```text
|
||||||
|
Capsule drops from brick: Catch with paddle:
|
||||||
|
|
||||||
|
[brick] |C|
|
||||||
|
|C| =============
|
||||||
|
|C| ^^^paddle^^^
|
||||||
|
|C|
|
||||||
|
v
|
||||||
|
=============
|
||||||
|
```
|
||||||
|
|
||||||
|
| Capsule | Color | Letter | Effect |
|
||||||
|
|---------|-------|--------|--------|
|
||||||
|
| Laser | `Red` | **L** | Paddle auto-fires lasers upward, destroying bricks on contact |
|
||||||
|
| Extend | `Blue` | **E** | Paddle width increases by 50% |
|
||||||
|
| Catch | `Green` | **C** | Ball sticks to paddle on contact, auto-releases after 3 seconds |
|
||||||
|
| Slow | `Orange` | **S** | Ball speed reduced to 60%, gradually recovers over 5 seconds |
|
||||||
|
| BreakOut | `Pink` | **B** | Opens exit on right wall — move paddle to exit for +10,000 pts |
|
||||||
|
| Split | `Cyan` | **D** | Splits ball into 3! Life lost only when ALL balls are gone |
|
||||||
|
| Extra Life | `Grey` | **+** | +1 life (rarest drop) |
|
||||||
|
|
||||||
|
Power-ups reset on life lost or level clear. No capsules drop while multiple balls are active.
|
||||||
|
|
||||||
|
## Brick Types
|
||||||
|
|
||||||
|
```text
|
||||||
|
Normal bricks: Silver bricks: Gold bricks:
|
||||||
|
+-----------+ +===========+ +###########+
|
||||||
|
| colored | | multi-hit | | unbreakable|
|
||||||
|
+-----------+ +===========+ +###########+
|
||||||
|
1 hit, gone 2-3 hits indestructible
|
||||||
|
50-120 pts 50 x level bounces ball
|
||||||
|
```
|
||||||
|
|
||||||
|
| Type | Appears | Hits | Score | Visual |
|
||||||
|
|------|---------|------|-------|--------|
|
||||||
|
| Normal | All levels | 1 | 50-120 (by color) | Colored, no border |
|
||||||
|
| Silver | Level 3+ | 2 (lvl 3-6), 3 (lvl 7+) | 50 x level | Grey with white border, darkens on hit |
|
||||||
|
| Gold | Level 5+ | Indestructible | - | Amber with gold border |
|
||||||
|
|
||||||
|
## Scoring
|
||||||
|
|
||||||
|
| Source | Points |
|
||||||
|
|--------|--------|
|
||||||
|
| Purple brick | 50 |
|
||||||
|
| Orange brick | 60 |
|
||||||
|
| Cyan brick | 70 |
|
||||||
|
| Green brick | 80 |
|
||||||
|
| Red brick | 90 |
|
||||||
|
| Blue brick | 100 |
|
||||||
|
| Pink brick | 110 |
|
||||||
|
| Yellow brick | 120 |
|
||||||
|
| Silver brick | 50 x level |
|
||||||
|
| BreakOut exit | 10,000 |
|
||||||
|
|
||||||
|
Ball speed increases by +0.15 for every 5 bricks destroyed, and +0.3 per level.
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
**Touchscreen:** Touch/drag to move paddle, tap to launch ball or release caught ball
|
||||||
|
**Trackball:** Move trackball left and right to move paddle, press to launch ball or release caught ball
|
||||||
|
|
||||||
|
**Keyboard:**
|
||||||
|
| Key | Action |
|
||||||
|
|-----|--------|
|
||||||
|
| `LEFT` / `a` / `,` | Move paddle left |
|
||||||
|
| `RIGHT` / `d` / `/` | Move paddle right |
|
||||||
|
| `SPACE` / `ENTER` | Launch ball / release caught ball / pause |
|
||||||
|
|
||||||
|
**Toolbar:** Pause button and sound toggle button
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Tactility SDK 0.7.0-dev
|
||||||
|
- ESP32, ESP32-S3, ESP32-C6, or ESP32-P4
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tactility.py build
|
||||||
|
# or build + install + run:
|
||||||
|
python tactility.py bir <device-ip>
|
||||||
|
```
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||||
|
file(GLOB_RECURSE SFX_ENGINE_FILES ../../../Libraries/SfxEngine/Source/*.c*)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES} ${SFX_ENGINE_FILES}
|
||||||
|
# Library headers must be included directly,
|
||||||
|
# because all regular dependencies get stripped by elf_loader's cmake script
|
||||||
|
INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include ../../../Libraries/SfxEngine/Include
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
|||||||
|
/**
|
||||||
|
* @file Breakout.h
|
||||||
|
* @brief Breakout arcade game for Tactility
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
class SfxEngine;
|
||||||
|
|
||||||
|
enum class GameState { Ready, Playing, Paused, GameOver };
|
||||||
|
enum class BrickType : uint8_t { Normal, Silver, Gold };
|
||||||
|
enum class PowerUpType : uint8_t { Laser, Extend, Catch, Slow, BreakOut, Split, ExtraLife };
|
||||||
|
|
||||||
|
static constexpr int TICK_MS = 25;
|
||||||
|
static constexpr int MAX_COLS = 12;
|
||||||
|
static constexpr int MAX_ROWS = 5;
|
||||||
|
static constexpr int MAX_BRICKS = MAX_COLS * MAX_ROWS;
|
||||||
|
static constexpr int INITIAL_LIVES = 10;
|
||||||
|
static constexpr int MAX_CAPSULES = 4;
|
||||||
|
static constexpr int MAX_BALLS = 3;
|
||||||
|
static constexpr int MAX_LASERS = 2;
|
||||||
|
|
||||||
|
// Per-color scoring (Purple=50 through Yellow=120)
|
||||||
|
static constexpr int COLOR_SCORES[] = { 50, 60, 70, 80, 90, 100, 110, 120 };
|
||||||
|
|
||||||
|
struct Capsule {
|
||||||
|
float x, y;
|
||||||
|
PowerUpType type;
|
||||||
|
bool active;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BallState {
|
||||||
|
float x, y, vx, vy;
|
||||||
|
bool active;
|
||||||
|
lv_obj_t* obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Laser {
|
||||||
|
float x, y;
|
||||||
|
bool active;
|
||||||
|
lv_obj_t* obj;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Breakout final : public App {
|
||||||
|
|
||||||
|
private:
|
||||||
|
// UI pointers (nulled in onHide)
|
||||||
|
lv_obj_t* gameArea = nullptr;
|
||||||
|
lv_obj_t* paddle = nullptr;
|
||||||
|
lv_obj_t* bricks[MAX_BRICKS] = {};
|
||||||
|
lv_obj_t* scoreLabel = nullptr;
|
||||||
|
lv_obj_t* livesLabel = nullptr;
|
||||||
|
lv_obj_t* messageLabel = nullptr;
|
||||||
|
lv_timer_t* gameTimer = nullptr;
|
||||||
|
lv_obj_t* soundBtnIcon = nullptr;
|
||||||
|
|
||||||
|
// Capsule UI
|
||||||
|
lv_obj_t* capsuleObjs[MAX_CAPSULES] = {};
|
||||||
|
lv_obj_t* capsuleLabels[MAX_CAPSULES] = {};
|
||||||
|
|
||||||
|
// Laser UI
|
||||||
|
Laser lasers[MAX_LASERS] = {};
|
||||||
|
|
||||||
|
// BreakOut exit
|
||||||
|
lv_obj_t* exitIndicator = nullptr;
|
||||||
|
|
||||||
|
// Sfx
|
||||||
|
SfxEngine* sfxEngine = nullptr;
|
||||||
|
|
||||||
|
// Game state (persists across hide/show)
|
||||||
|
GameState state = GameState::Ready;
|
||||||
|
int score = 0;
|
||||||
|
int lives = INITIAL_LIVES;
|
||||||
|
int level = 1;
|
||||||
|
int bricksRemaining = 0;
|
||||||
|
int destroyedCount = 0;
|
||||||
|
bool brickAlive[MAX_BRICKS] = {};
|
||||||
|
BrickType brickType[MAX_BRICKS] = {};
|
||||||
|
int brickHits[MAX_BRICKS] = {};
|
||||||
|
int brickColorIndex[MAX_BRICKS] = {};
|
||||||
|
bool needsInit = true;
|
||||||
|
|
||||||
|
// Multi-ball
|
||||||
|
BallState balls[MAX_BALLS] = {};
|
||||||
|
int activeBallCount = 1;
|
||||||
|
|
||||||
|
// Capsules
|
||||||
|
Capsule capsules[MAX_CAPSULES] = {};
|
||||||
|
int capsuleW = 0, capsuleH = 0;
|
||||||
|
float capsuleFallSpeed = 0;
|
||||||
|
|
||||||
|
// Power-up state
|
||||||
|
bool catchActive = false;
|
||||||
|
int catchBallIndex = -1;
|
||||||
|
float catchOffsetX = 0;
|
||||||
|
int catchAutoReleaseTicks = 0;
|
||||||
|
|
||||||
|
float originalBallSpeed = 0;
|
||||||
|
int slowRecoveryTicks = 0;
|
||||||
|
|
||||||
|
int originalPaddleW = 0;
|
||||||
|
bool extendActive = false;
|
||||||
|
|
||||||
|
bool laserActive = false;
|
||||||
|
int laserCooldown = 0;
|
||||||
|
int laserW = 0, laserH = 0;
|
||||||
|
float laserSpeed = 0;
|
||||||
|
|
||||||
|
bool exitOpen = false;
|
||||||
|
|
||||||
|
float baseBallSpeed = 0;
|
||||||
|
|
||||||
|
// Paddle
|
||||||
|
float paddleX = 0;
|
||||||
|
|
||||||
|
// Layout dimensions (calculated in onShow)
|
||||||
|
int areaW = 0, areaH = 0;
|
||||||
|
int cols = 0, rows = 0;
|
||||||
|
int brickW = 0, brickH = 0;
|
||||||
|
int brickGap = 0;
|
||||||
|
int brickOffsetX = 0, brickOffsetY = 0;
|
||||||
|
int ballSize = 0;
|
||||||
|
int paddleW = 0, paddleH = 0;
|
||||||
|
int paddleYPos = 0;
|
||||||
|
float ballSpeed = 0;
|
||||||
|
float paddleSpeed = 0;
|
||||||
|
|
||||||
|
// Static callbacks
|
||||||
|
static void onTick(lv_timer_t* timer);
|
||||||
|
static void onPressed(lv_event_t* e);
|
||||||
|
static void onClicked(lv_event_t* e);
|
||||||
|
static void onKey(lv_event_t* e);
|
||||||
|
static void onFocused(lv_event_t* e);
|
||||||
|
static void onPauseClicked(lv_event_t* e);
|
||||||
|
static void onSoundToggled(lv_event_t* e);
|
||||||
|
|
||||||
|
// Game logic
|
||||||
|
void startGame();
|
||||||
|
void nextLevel();
|
||||||
|
void resetBall();
|
||||||
|
void launchBall();
|
||||||
|
void update();
|
||||||
|
void checkLaserBrickCollisions();
|
||||||
|
void loseLife();
|
||||||
|
void winLevel();
|
||||||
|
void createBricks();
|
||||||
|
void setupLevelPattern();
|
||||||
|
void refreshBricks();
|
||||||
|
void updateScoreDisplay();
|
||||||
|
void updateMessage();
|
||||||
|
void togglePause();
|
||||||
|
void updateSoundIcon();
|
||||||
|
|
||||||
|
// Capsule system
|
||||||
|
void spawnCapsule(float x, float y);
|
||||||
|
void updateCapsules();
|
||||||
|
void activatePowerUp(PowerUpType type);
|
||||||
|
void clearPowerUps();
|
||||||
|
void createCapsuleObjs();
|
||||||
|
|
||||||
|
// Multi-ball
|
||||||
|
void updateBalls();
|
||||||
|
void splitBalls();
|
||||||
|
|
||||||
|
// Laser
|
||||||
|
void updateLasers();
|
||||||
|
void fireLaser();
|
||||||
|
void createLaserObjs();
|
||||||
|
|
||||||
|
// BreakOut exit
|
||||||
|
void openExit();
|
||||||
|
void closeExit();
|
||||||
|
|
||||||
|
// Brick helpers
|
||||||
|
void hitBrick(int idx);
|
||||||
|
int scoreBrick(int idx);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void onShow(AppHandle context, lv_obj_t* parent) override;
|
||||||
|
void onHide(AppHandle context) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include "Breakout.h"
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
registerApp<Breakout>();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.7.0-dev
|
||||||
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
|
[app]
|
||||||
|
id=one.tactility.breakout
|
||||||
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=Breakout
|
||||||
|
description=Classic brick-breaking arcade game
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(Magic8Ball)
|
||||||
|
tactility_project(Magic8Ball)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Magic 8-Ball
|
||||||
|
|
||||||
|
A digital fortune-telling toy for Tactility.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Ask a yes/no question, then shake the ball (tap or press a key) to receive one of 20 classic Magic 8-Ball responses. Responses span affirmative, non-committal, and negative categories.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 20 classic Magic 8-Ball responses
|
||||||
|
- Three response categories: Affirmative (10), Non-committal (5), Negative (5)
|
||||||
|
- Prevents consecutive duplicate answers
|
||||||
|
- Animated ball interface
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- **Touch**: Tap the ball to get a response
|
||||||
|
- **Keyboard**: Enter or Space
|
||||||
|
|
||||||
|
## Responses
|
||||||
|
|
||||||
|
- **Affirmative**: "It is certain", "Without a doubt", "Yes definitely", "Most likely", and more
|
||||||
|
- **Non-committal**: "Reply hazy, try again", "Ask again later", "Better not tell you now", and more
|
||||||
|
- **Negative**: "Don't count on it", "My reply is no", "Very doubtful", and more
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Tactility
|
||||||
|
- Input: Touchscreen and/or keyboard
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES
|
||||||
|
Source/*.c*
|
||||||
|
)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
# Library headers must be included directly,
|
||||||
|
# because all regular dependencies get stripped by elf_loader's cmake script
|
||||||
|
INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#include "Magic8Ball.h"
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
#include <tt_lvgl_keyboard.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
/* ── Responses ─────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static const char* responses[] = {
|
||||||
|
/* Affirmative (10) */
|
||||||
|
"It is certain.",
|
||||||
|
"It is decidedly so.",
|
||||||
|
"Without a doubt.",
|
||||||
|
"Yes, definitely.",
|
||||||
|
"You may rely on it.",
|
||||||
|
"As I see it, yes.",
|
||||||
|
"Most likely.",
|
||||||
|
"Outlook good.",
|
||||||
|
"Yes.",
|
||||||
|
"Signs point to yes.",
|
||||||
|
/* Non-committal (5) */
|
||||||
|
"Reply hazy, try again.",
|
||||||
|
"Ask again later.",
|
||||||
|
"Better not tell you now.",
|
||||||
|
"Cannot predict now.",
|
||||||
|
"Concentrate and ask again.",
|
||||||
|
/* Negative (5) */
|
||||||
|
"Don't count on it.",
|
||||||
|
"My reply is no.",
|
||||||
|
"My sources say no.",
|
||||||
|
"Outlook not so good.",
|
||||||
|
"Very doubtful.",
|
||||||
|
};
|
||||||
|
|
||||||
|
#define NUM_RESPONSES (sizeof(responses) / sizeof(responses[0]))
|
||||||
|
|
||||||
|
static const char* getInputHint() {
|
||||||
|
if (tt_lvgl_hardware_keyboard_is_available()) {
|
||||||
|
return "Touch or press Space to ask";
|
||||||
|
}
|
||||||
|
return "Touch the ball to ask";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Methods ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void Magic8Ball::revealAnswer() {
|
||||||
|
if (!seeded) {
|
||||||
|
srand((unsigned)time(NULL));
|
||||||
|
seeded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx;
|
||||||
|
do {
|
||||||
|
idx = rand() % NUM_RESPONSES;
|
||||||
|
} while (idx == lastIdx && NUM_RESPONSES > 1);
|
||||||
|
lastIdx = idx;
|
||||||
|
|
||||||
|
lv_label_set_text(answerLabel, responses[idx]);
|
||||||
|
lv_label_set_text(hintLabel, getInputHint());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Magic8Ball::onBallClick(lv_event_t* e) {
|
||||||
|
auto* self = (Magic8Ball*)lv_event_get_user_data(e);
|
||||||
|
self->revealAnswer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Magic8Ball::onKey(lv_event_t* e) {
|
||||||
|
auto* self = (Magic8Ball*)lv_event_get_user_data(e);
|
||||||
|
uint32_t key = lv_event_get_key(e);
|
||||||
|
if (key == LV_KEY_ENTER || key == ' ') {
|
||||||
|
self->revealAnswer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Lifecycle ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void Magic8Ball::onShow(AppHandle app, lv_obj_t* parent) {
|
||||||
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
|
/* Toolbar */
|
||||||
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||||
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
|
/* Main container */
|
||||||
|
lv_obj_t* cont = lv_obj_create(parent);
|
||||||
|
lv_obj_set_width(cont, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(cont, 1);
|
||||||
|
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_align_to(cont, toolbar, LV_ALIGN_OUT_BOTTOM_MID, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(cont, 2, 0);
|
||||||
|
lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_style_border_width(cont, 0, 0);
|
||||||
|
lv_obj_set_style_bg_color(cont, lv_color_hex(0x000000), 0);
|
||||||
|
lv_obj_set_style_bg_opa(cont, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_radius(cont, 0, 0);
|
||||||
|
|
||||||
|
/* "8" ball circle */
|
||||||
|
ballObj = lv_obj_create(cont);
|
||||||
|
lv_obj_set_size(ballObj, 120, 120);
|
||||||
|
lv_obj_set_style_radius(ballObj, LV_RADIUS_CIRCLE, 0);
|
||||||
|
lv_obj_set_style_bg_color(ballObj, lv_color_make(0x10, 0x10, 0x30), 0);
|
||||||
|
lv_obj_set_style_bg_opa(ballObj, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_border_color(ballObj, lv_color_make(0x40, 0x40, 0x80), 0);
|
||||||
|
lv_obj_set_style_border_width(ballObj, 3, 0);
|
||||||
|
lv_obj_remove_flag(ballObj, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_flex_flow(ballObj, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(ballObj, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
|
||||||
|
/* Answer text inside the ball */
|
||||||
|
answerLabel = lv_label_create(ballObj);
|
||||||
|
lv_label_set_text(answerLabel, "8");
|
||||||
|
lv_obj_set_style_text_color(answerLabel, lv_color_hex(0xFFFFFF), 0);
|
||||||
|
lv_obj_set_style_text_font(answerLabel, lv_font_get_default(), 0);
|
||||||
|
lv_obj_set_style_text_align(answerLabel, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
|
lv_obj_set_width(answerLabel, 100);
|
||||||
|
lv_label_set_long_mode(answerLabel, LV_LABEL_LONG_WRAP);
|
||||||
|
|
||||||
|
/* Hint text below the ball */
|
||||||
|
hintLabel = lv_label_create(cont);
|
||||||
|
lv_label_set_text(hintLabel, getInputHint());
|
||||||
|
lv_obj_set_style_text_color(hintLabel, lv_color_make(0x88, 0x88, 0x88), 0);
|
||||||
|
lv_obj_set_style_text_font(hintLabel, lv_font_get_default(), 0);
|
||||||
|
|
||||||
|
/* Make the ball tappable / also space */
|
||||||
|
lv_obj_add_flag(ballObj, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_event_cb(ballObj, onBallClick, LV_EVENT_CLICKED, this);
|
||||||
|
|
||||||
|
/* Keyboard support */
|
||||||
|
lv_group_t* grp = lv_group_get_default();
|
||||||
|
if (grp) lv_group_add_obj(grp, ballObj);
|
||||||
|
lv_obj_add_event_cb(ballObj, onKey, LV_EVENT_KEY, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Magic8Ball::onHide(AppHandle app) {
|
||||||
|
lv_group_t* grp = lv_group_get_default();
|
||||||
|
if (grp && ballObj) {
|
||||||
|
lv_group_remove_obj(ballObj);
|
||||||
|
}
|
||||||
|
answerLabel = nullptr;
|
||||||
|
hintLabel = nullptr;
|
||||||
|
ballObj = nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
class Magic8Ball final : public App {
|
||||||
|
|
||||||
|
private:
|
||||||
|
// UI pointers (nulled in onHide)
|
||||||
|
lv_obj_t* answerLabel = nullptr;
|
||||||
|
lv_obj_t* hintLabel = nullptr;
|
||||||
|
lv_obj_t* ballObj = nullptr;
|
||||||
|
|
||||||
|
// State
|
||||||
|
int lastIdx = -1;
|
||||||
|
bool seeded = false;
|
||||||
|
|
||||||
|
void revealAnswer();
|
||||||
|
|
||||||
|
static void onBallClick(lv_event_t* e);
|
||||||
|
static void onKey(lv_event_t* e);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void onShow(AppHandle context, lv_obj_t* parent) override;
|
||||||
|
void onHide(AppHandle context) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include "Magic8Ball.h"
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
registerApp<Magic8Ball>();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.7.0-dev
|
||||||
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
|
[app]
|
||||||
|
id=one.tactility.magic8ball
|
||||||
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=Magic 8-Ball
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
if (DEFINED ENV{TACTILITY_SDK_PATH})
|
||||||
|
set(TACTILITY_SDK_PATH $ENV{TACTILITY_SDK_PATH})
|
||||||
|
else()
|
||||||
|
set(TACTILITY_SDK_PATH "../../release/TactilitySDK")
|
||||||
|
message(WARNING "⚠️ TACTILITY_SDK_PATH environment variable is not set, defaulting to ${TACTILITY_SDK_PATH}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include("${TACTILITY_SDK_PATH}/TactilitySDK.cmake")
|
||||||
|
set(EXTRA_COMPONENT_DIRS ${TACTILITY_SDK_PATH})
|
||||||
|
|
||||||
|
project(TodoList)
|
||||||
|
tactility_project(TodoList)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Todo List
|
||||||
|
|
||||||
|
A simple task list manager for Tactility.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Keep track of tasks directly on your device. Add items, mark them complete, and delete them when done. Tasks are automatically saved and persist across sessions.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Add, complete, and delete tasks
|
||||||
|
- Visual completion indicators (checkmarks vs pending dots)
|
||||||
|
- Strike-through styling for completed items
|
||||||
|
- Pending task counter in header
|
||||||
|
- Clear all completed tasks at once
|
||||||
|
- Persistent storage (survives reboots)
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
- **Touch**:
|
||||||
|
- Tap a task: Toggle between done and pending
|
||||||
|
- Tap X button: Delete a task
|
||||||
|
- **Text Input**: Type a task name, then press Enter or the + button to add it
|
||||||
|
- **Toolbar Button**: Trash icon to clear all completed tasks
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
Tasks are saved to the app's user data directory in a simple text format:
|
||||||
|
- `-` prefix = pending task
|
||||||
|
- `+` prefix = completed task
|
||||||
|
|
||||||
|
Up to 50 tasks, 128 characters each.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Tactility
|
||||||
|
- Touchscreen or keyboard
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
file(GLOB_RECURSE SOURCE_FILES
|
||||||
|
Source/*.c*
|
||||||
|
)
|
||||||
|
|
||||||
|
idf_component_register(
|
||||||
|
SRCS ${SOURCE_FILES}
|
||||||
|
# Library headers must be included directly,
|
||||||
|
# because all regular dependencies get stripped by elf_loader's cmake script
|
||||||
|
INCLUDE_DIRS ../../../Libraries/TactilityCpp/Include
|
||||||
|
REQUIRES TactilitySDK
|
||||||
|
)
|
||||||
@@ -0,0 +1,366 @@
|
|||||||
|
#include "TodoList.h"
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <tt_lock.h>
|
||||||
|
#include <Tactility/kernel/Kernel.h>
|
||||||
|
#include <tt_lvgl_toolbar.h>
|
||||||
|
#include <tt_lvgl_keyboard.h>
|
||||||
|
#include <tactility/lvgl_module.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static constexpr const char* SAVE_FILENAME = "todos.txt";
|
||||||
|
|
||||||
|
/* File-scope instance pointer for index-based callbacks */
|
||||||
|
static TodoList* g_instance = nullptr;
|
||||||
|
static AppHandle s_appHandle = nullptr;
|
||||||
|
|
||||||
|
/* ── Helpers ──────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static bool getSaveFilePath(char* buf, size_t bufSize) {
|
||||||
|
if (!s_appHandle) return false;
|
||||||
|
size_t size = bufSize;
|
||||||
|
tt_app_get_user_data_child_path(s_appHandle, SAVE_FILENAME, buf, &size);
|
||||||
|
return size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ensureDir() {
|
||||||
|
if (!s_appHandle) return false;
|
||||||
|
char dir[256];
|
||||||
|
size_t size = sizeof(dir);
|
||||||
|
tt_app_get_user_data_path(s_appHandle, dir, &size);
|
||||||
|
if (size == 0) return false;
|
||||||
|
mkdir(dir, 0755);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int getToolbarHeight(UiDensity density) {
|
||||||
|
return (density == LVGL_UI_DENSITY_COMPACT) ? 22 : 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Persistence ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void TodoList::saveTodos() {
|
||||||
|
if (!ensureDir()) return;
|
||||||
|
char savePath[256];
|
||||||
|
if (!getSaveFilePath(savePath, sizeof(savePath))) return;
|
||||||
|
|
||||||
|
auto lock = tt_lock_alloc_for_path(savePath);
|
||||||
|
if (!lock) return;
|
||||||
|
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
|
||||||
|
FILE* f = fopen(savePath, "w");
|
||||||
|
if (f) {
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
fprintf(f, "%c %s\n", items[i].done ? '+' : '-', items[i].text);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
tt_lock_release(lock);
|
||||||
|
}
|
||||||
|
tt_lock_free(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::loadTodos() {
|
||||||
|
char savePath[256];
|
||||||
|
if (!getSaveFilePath(savePath, sizeof(savePath))) return;
|
||||||
|
|
||||||
|
auto lock = tt_lock_alloc_for_path(savePath);
|
||||||
|
if (!lock) return;
|
||||||
|
|
||||||
|
if (tt_lock_acquire(lock, tt::kernel::MAX_TICKS)) {
|
||||||
|
count = 0;
|
||||||
|
FILE* f = fopen(savePath, "r");
|
||||||
|
if (f) {
|
||||||
|
char line[MAX_TEXT_LEN + 4];
|
||||||
|
while (count < MAX_TODOS && fgets(line, sizeof(line), f)) {
|
||||||
|
size_t len = strlen(line);
|
||||||
|
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
|
||||||
|
line[--len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len < 3 || line[1] != ' ') continue;
|
||||||
|
|
||||||
|
TodoItem* item = &items[count];
|
||||||
|
item->done = (line[0] == '+');
|
||||||
|
strncpy(item->text, &line[2], MAX_TEXT_LEN - 1);
|
||||||
|
item->text[MAX_TEXT_LEN - 1] = '\0';
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
tt_lock_release(lock);
|
||||||
|
}
|
||||||
|
tt_lock_free(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── UI Helpers ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void TodoList::updateCountLabel() {
|
||||||
|
if (!countLabel) return;
|
||||||
|
int pending = 0;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
if (!items[i].done) pending++;
|
||||||
|
}
|
||||||
|
if (pending > 0) {
|
||||||
|
lv_label_set_text_fmt(countLabel, "%d left", pending);
|
||||||
|
} else if (count > 0) {
|
||||||
|
lv_label_set_text(countLabel, "All done!");
|
||||||
|
} else {
|
||||||
|
lv_label_set_text(countLabel, "No tasks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::addItem(const char* text) {
|
||||||
|
if (!text || !text[0]) return;
|
||||||
|
if (count >= MAX_TODOS) return;
|
||||||
|
|
||||||
|
while (*text == ' ') text++;
|
||||||
|
if (!*text) return;
|
||||||
|
|
||||||
|
TodoItem* item = &items[count];
|
||||||
|
item->done = false;
|
||||||
|
strncpy(item->text, text, MAX_TEXT_LEN - 1);
|
||||||
|
item->text[MAX_TEXT_LEN - 1] = '\0';
|
||||||
|
|
||||||
|
size_t len = strlen(item->text);
|
||||||
|
while (len > 0 && item->text[len - 1] == ' ') {
|
||||||
|
item->text[--len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
saveTodos();
|
||||||
|
rebuildList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::scheduleRebuild() {
|
||||||
|
if (rebuildPending) return;
|
||||||
|
rebuildPending = true;
|
||||||
|
rebuildTimer = lv_timer_create(onDeferredRebuild, 0, this);
|
||||||
|
if (!rebuildTimer) {
|
||||||
|
rebuildPending = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lv_timer_set_repeat_count(rebuildTimer, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::onDeferredRebuild(lv_timer_t* timer) {
|
||||||
|
TodoList* self = static_cast<TodoList*>(lv_timer_get_user_data(timer));
|
||||||
|
if (self) {
|
||||||
|
self->rebuildTimer = nullptr;
|
||||||
|
self->rebuildPending = false;
|
||||||
|
self->rebuildList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::rebuildList() {
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
lv_obj_clean(list);
|
||||||
|
|
||||||
|
UiDensity uiDensity = lvgl_get_ui_density();
|
||||||
|
int toolbarHeight = getToolbarHeight(uiDensity);
|
||||||
|
int btnSize = (uiDensity == LVGL_UI_DENSITY_COMPACT) ? toolbarHeight - 8 : toolbarHeight - 6;
|
||||||
|
|
||||||
|
if (count == 0) {
|
||||||
|
lv_list_add_text(list, "No tasks yet. Add one below!");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
TodoItem* item = &items[i];
|
||||||
|
|
||||||
|
char display[MAX_TEXT_LEN + 8];
|
||||||
|
snprintf(display, sizeof(display), "%s %s", item->done ? LV_SYMBOL_OK : LV_SYMBOL_DUMMY, item->text);
|
||||||
|
|
||||||
|
lv_obj_t* btn = lv_list_add_button(list, NULL, display);
|
||||||
|
|
||||||
|
if (item->done) {
|
||||||
|
lv_obj_set_style_text_opa(btn, LV_OPA_50, LV_PART_MAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_add_event_cb(btn, onItemClicked, LV_EVENT_SHORT_CLICKED, (void*)(intptr_t)i);
|
||||||
|
|
||||||
|
lv_obj_t* delBtn = lv_button_create(btn);
|
||||||
|
lv_obj_set_size(delBtn, btnSize, btnSize);
|
||||||
|
lv_obj_align(delBtn, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||||
|
lv_obj_set_style_bg_color(delBtn, lv_palette_main(LV_PALETTE_RED), LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_pad_all(delBtn, 0, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_shadow_width(delBtn, 0, LV_PART_MAIN);
|
||||||
|
|
||||||
|
lv_obj_t* delIcon = lv_label_create(delBtn);
|
||||||
|
lv_label_set_text(delIcon, LV_SYMBOL_CLOSE);
|
||||||
|
lv_obj_center(delIcon);
|
||||||
|
|
||||||
|
lv_obj_add_event_cb(delBtn, onDeleteClicked, LV_EVENT_CLICKED, (void*)(intptr_t)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCountLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Callbacks ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void TodoList::onItemClicked(lv_event_t* e) {
|
||||||
|
if (!g_instance) return;
|
||||||
|
int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||||
|
if (idx < 0 || idx >= g_instance->count) return;
|
||||||
|
|
||||||
|
g_instance->items[idx].done = !g_instance->items[idx].done;
|
||||||
|
g_instance->saveTodos();
|
||||||
|
g_instance->scheduleRebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::onDeleteClicked(lv_event_t* e) {
|
||||||
|
if (!g_instance) return;
|
||||||
|
|
||||||
|
int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||||
|
if (idx < 0 || idx >= g_instance->count) return;
|
||||||
|
|
||||||
|
for (int i = idx; i < g_instance->count - 1; i++) {
|
||||||
|
g_instance->items[i] = g_instance->items[i + 1];
|
||||||
|
}
|
||||||
|
g_instance->count--;
|
||||||
|
|
||||||
|
g_instance->saveTodos();
|
||||||
|
g_instance->scheduleRebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::onAddClicked(lv_event_t* e) {
|
||||||
|
if (!g_instance || !g_instance->inputTa) return;
|
||||||
|
const char* text = lv_textarea_get_text(g_instance->inputTa);
|
||||||
|
g_instance->addItem(text);
|
||||||
|
lv_textarea_set_text(g_instance->inputTa, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::onInputReady(lv_event_t* e) {
|
||||||
|
onAddClicked(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::onClearDoneClicked(lv_event_t* e) {
|
||||||
|
if (!g_instance) return;
|
||||||
|
int write = 0;
|
||||||
|
for (int read = 0; read < g_instance->count; read++) {
|
||||||
|
if (!g_instance->items[read].done) {
|
||||||
|
if (write != read) {
|
||||||
|
g_instance->items[write] = g_instance->items[read];
|
||||||
|
}
|
||||||
|
write++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g_instance->count = write;
|
||||||
|
g_instance->saveTodos();
|
||||||
|
g_instance->scheduleRebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Lifecycle ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
void TodoList::onShow(AppHandle app, lv_obj_t* parent) {
|
||||||
|
g_instance = this;
|
||||||
|
s_appHandle = app;
|
||||||
|
|
||||||
|
loadTodos();
|
||||||
|
|
||||||
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
|
/* Toolbar */
|
||||||
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
||||||
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
||||||
|
|
||||||
|
lv_obj_t* countWrapper = lv_obj_create(toolbar);
|
||||||
|
lv_obj_set_size(countWrapper, LV_SIZE_CONTENT, LV_PCT(100));
|
||||||
|
lv_obj_set_style_pad_top(countWrapper, 4, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_bottom(countWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_left(countWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_right(countWrapper, 4, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_row(countWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_column(countWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(countWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(countWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_remove_flag(countWrapper, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
countLabel = lv_label_create(countWrapper);
|
||||||
|
lv_obj_set_size(countLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_align(countLabel, LV_ALIGN_CENTER, 0, 0);
|
||||||
|
lv_obj_set_style_text_align(countLabel, LV_TEXT_ALIGN_LEFT, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_text_font(countLabel, lv_font_get_default(), 0);
|
||||||
|
lv_obj_set_style_text_color(countLabel, lv_palette_main(LV_PALETTE_CYAN), LV_PART_MAIN);
|
||||||
|
|
||||||
|
UiDensity uiDensity = lvgl_get_ui_density();
|
||||||
|
int toolbarHeight = getToolbarHeight(uiDensity);
|
||||||
|
|
||||||
|
lv_obj_t* btnWrapper = lv_obj_create(toolbar);
|
||||||
|
lv_obj_set_width(btnWrapper, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(btnWrapper, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_style_pad_all(btnWrapper, 2, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_pad_column(btnWrapper, 4, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_border_width(btnWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_set_style_bg_opa(btnWrapper, 0, LV_STATE_DEFAULT);
|
||||||
|
|
||||||
|
int btnSize = (uiDensity == LVGL_UI_DENSITY_COMPACT)
|
||||||
|
? toolbarHeight - 8
|
||||||
|
: toolbarHeight - 6;
|
||||||
|
|
||||||
|
lv_obj_t* clearBtn = lv_button_create(btnWrapper);
|
||||||
|
lv_obj_set_size(clearBtn, btnSize, btnSize);
|
||||||
|
lv_obj_set_style_pad_all(clearBtn, 0, LV_STATE_DEFAULT);
|
||||||
|
lv_obj_align(clearBtn, LV_ALIGN_CENTER, 0, 0);
|
||||||
|
lv_obj_add_event_cb(clearBtn, onClearDoneClicked, LV_EVENT_CLICKED, nullptr);
|
||||||
|
|
||||||
|
lv_obj_t* clearIcon = lv_label_create(clearBtn);
|
||||||
|
lv_label_set_text(clearIcon, LV_SYMBOL_TRASH);
|
||||||
|
lv_obj_align(clearIcon, LV_ALIGN_CENTER, 0, 0);
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
lv_obj_t* cont = lv_obj_create(parent);
|
||||||
|
lv_obj_set_width(cont, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(cont, 1);
|
||||||
|
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_all(cont, 4, 0);
|
||||||
|
lv_obj_set_style_pad_gap(cont, 4, 0);
|
||||||
|
lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_style_border_width(cont, 0, 0);
|
||||||
|
|
||||||
|
/* Scrollable list */
|
||||||
|
list = lv_list_create(cont);
|
||||||
|
lv_obj_set_width(list, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(list, 1);
|
||||||
|
|
||||||
|
/* Input row */
|
||||||
|
inputRow = lv_obj_create(cont);
|
||||||
|
lv_obj_set_size(inputRow, LV_PCT(100), LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(inputRow, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(inputRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_all(inputRow, 0, 0);
|
||||||
|
lv_obj_set_style_pad_gap(inputRow, 4, 0);
|
||||||
|
lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_set_style_border_width(inputRow, 0, 0);
|
||||||
|
|
||||||
|
inputTa = lv_textarea_create(inputRow);
|
||||||
|
lv_textarea_set_placeholder_text(inputTa, "New task...");
|
||||||
|
lv_textarea_set_one_line(inputTa, true);
|
||||||
|
lv_obj_set_flex_grow(inputTa, 1);
|
||||||
|
lv_obj_set_style_text_font(inputTa, lv_font_get_default(), 0);
|
||||||
|
lv_obj_add_event_cb(inputTa, onInputReady, LV_EVENT_READY, nullptr);
|
||||||
|
|
||||||
|
lv_obj_t* addBtn = lv_button_create(inputRow);
|
||||||
|
lv_obj_t* addLbl = lv_label_create(addBtn);
|
||||||
|
lv_label_set_text(addLbl, LV_SYMBOL_PLUS);
|
||||||
|
lv_obj_add_event_cb(addBtn, onAddClicked, LV_EVENT_CLICKED, nullptr);
|
||||||
|
|
||||||
|
rebuildList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TodoList::onHide(AppHandle app) {
|
||||||
|
if (rebuildTimer) {
|
||||||
|
lv_timer_delete(rebuildTimer);
|
||||||
|
rebuildTimer = nullptr;
|
||||||
|
}
|
||||||
|
rebuildPending = false;
|
||||||
|
list = nullptr;
|
||||||
|
inputRow = nullptr;
|
||||||
|
inputTa = nullptr;
|
||||||
|
countLabel = nullptr;
|
||||||
|
g_instance = nullptr;
|
||||||
|
s_appHandle = nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <tt_app.h>
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
class TodoList final : public App {
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
static constexpr int MAX_TODOS = 50;
|
||||||
|
static constexpr int MAX_TEXT_LEN = 128;
|
||||||
|
|
||||||
|
struct TodoItem {
|
||||||
|
char text[MAX_TEXT_LEN];
|
||||||
|
bool done;
|
||||||
|
};
|
||||||
|
|
||||||
|
// UI pointers (nulled in onHide)
|
||||||
|
lv_obj_t* list = nullptr;
|
||||||
|
lv_obj_t* inputRow = nullptr;
|
||||||
|
lv_obj_t* inputTa = nullptr;
|
||||||
|
lv_obj_t* countLabel = nullptr;
|
||||||
|
|
||||||
|
// Data
|
||||||
|
TodoItem items[MAX_TODOS] = {};
|
||||||
|
int count = 0;
|
||||||
|
bool rebuildPending = false;
|
||||||
|
lv_timer_t* rebuildTimer = nullptr;
|
||||||
|
|
||||||
|
// Persistence
|
||||||
|
void saveTodos();
|
||||||
|
void loadTodos();
|
||||||
|
|
||||||
|
// UI helpers
|
||||||
|
void updateCountLabel();
|
||||||
|
void rebuildList();
|
||||||
|
void scheduleRebuild();
|
||||||
|
void addItem(const char* text);
|
||||||
|
|
||||||
|
static void onDeferredRebuild(lv_timer_t* timer);
|
||||||
|
|
||||||
|
// Static callbacks
|
||||||
|
static void onItemClicked(lv_event_t* e);
|
||||||
|
static void onDeleteClicked(lv_event_t* e);
|
||||||
|
static void onAddClicked(lv_event_t* e);
|
||||||
|
static void onInputReady(lv_event_t* e);
|
||||||
|
static void onClearDoneClicked(lv_event_t* e);
|
||||||
|
|
||||||
|
public:
|
||||||
|
void onShow(AppHandle context, lv_obj_t* parent) override;
|
||||||
|
void onHide(AppHandle context) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#include "TodoList.h"
|
||||||
|
#include <TactilityCpp/App.h>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
registerApp<TodoList>();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[manifest]
|
||||||
|
version=0.1
|
||||||
|
[target]
|
||||||
|
sdk=0.7.0-dev
|
||||||
|
platforms=esp32,esp32s3,esp32c6,esp32p4
|
||||||
|
[app]
|
||||||
|
id=one.tactility.todolist
|
||||||
|
versionName=0.1.0
|
||||||
|
versionCode=1
|
||||||
|
name=Todo List
|
||||||
|
description=Simple task list manager
|
||||||
Reference in New Issue
Block a user