2026-05-13 18:22:41 +02:00
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
|
|
#include <helpers/ui/DisplayDriver.h>
|
|
|
|
|
|
#include <Arduino.h>
|
|
|
|
|
|
|
|
|
|
|
|
// Layout constants shared by all keyboard users
|
|
|
|
|
|
static const char KB_CHARS[4][10] = {
|
|
|
|
|
|
{'a','b','c','d','e','f','g','h','i','j'},
|
|
|
|
|
|
{'k','l','m','n','o','p','q','r','s','t'},
|
|
|
|
|
|
{'u','v','w','x','y','z','.',' ','!','?'},
|
|
|
|
|
|
{'1','2','3','4','5','6','7','8','9','0'},
|
|
|
|
|
|
};
|
|
|
|
|
|
static const int KB_ROWS_CHAR = 4;
|
|
|
|
|
|
static const int KB_COLS_CHAR = 10;
|
|
|
|
|
|
static const int KB_SPECIAL = 5; // [^] [Sp] [Del] [{}] [OK]
|
|
|
|
|
|
static const char* KB_PH_LIST[] = { "{loc}", "{time}" };
|
|
|
|
|
|
static const int KB_PH_COUNT = 2;
|
|
|
|
|
|
static const int KB_MAX_LEN = 139;
|
|
|
|
|
|
static const int KB_CELL_W = 12; // 10 cols × 12px = 120px of 128px display
|
|
|
|
|
|
static const int KB_CELL_H = 9;
|
|
|
|
|
|
static const int KB_TEXT_Y = 0;
|
|
|
|
|
|
static const int KB_SEP_Y = 9;
|
|
|
|
|
|
static const int KB_CHARS_Y = 11;
|
|
|
|
|
|
static const int KB_SPECIAL_Y = 48;
|
|
|
|
|
|
|
|
|
|
|
|
struct KeyboardWidget {
|
|
|
|
|
|
char buf[KB_MAX_LEN + 1];
|
|
|
|
|
|
int len;
|
|
|
|
|
|
int max_len;
|
|
|
|
|
|
int row, col;
|
|
|
|
|
|
bool caps;
|
|
|
|
|
|
bool ph_mode;
|
|
|
|
|
|
int ph_sel;
|
|
|
|
|
|
|
|
|
|
|
|
enum Result { NONE, DONE, CANCELLED };
|
|
|
|
|
|
|
2026-05-13 18:34:33 +02:00
|
|
|
|
void begin(const char* initial = "", int max = KB_MAX_LEN) {
|
2026-05-13 18:22:41 +02:00
|
|
|
|
max_len = (max > KB_MAX_LEN) ? KB_MAX_LEN : max;
|
|
|
|
|
|
strncpy(buf, initial, max_len);
|
|
|
|
|
|
buf[max_len] = '\0';
|
|
|
|
|
|
len = strlen(buf);
|
|
|
|
|
|
row = col = 0;
|
|
|
|
|
|
caps = ph_mode = false;
|
|
|
|
|
|
ph_sel = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int render(DisplayDriver& display) {
|
|
|
|
|
|
display.setTextSize(1);
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
|
2026-05-13 18:34:33 +02:00
|
|
|
|
// text preview: last 20 chars + cursor
|
2026-05-13 18:22:41 +02:00
|
|
|
|
const char* disp_start = buf;
|
|
|
|
|
|
int disp_len = len;
|
|
|
|
|
|
if (disp_len > 20) { disp_start = buf + (disp_len - 20); disp_len = 20; }
|
2026-05-13 18:34:33 +02:00
|
|
|
|
char preview[24];
|
|
|
|
|
|
snprintf(preview, sizeof(preview), "%.*s_", disp_len, disp_start);
|
2026-05-13 18:22:41 +02:00
|
|
|
|
display.setCursor(0, KB_TEXT_Y);
|
|
|
|
|
|
display.print(preview);
|
|
|
|
|
|
display.fillRect(0, KB_SEP_Y, display.width(), 1);
|
|
|
|
|
|
|
|
|
|
|
|
// character grid
|
|
|
|
|
|
for (int r = 0; r < KB_ROWS_CHAR; r++) {
|
|
|
|
|
|
int y = KB_CHARS_Y + r * KB_CELL_H;
|
|
|
|
|
|
for (int c = 0; c < KB_COLS_CHAR; c++) {
|
|
|
|
|
|
bool sel = (row == r && col == c);
|
|
|
|
|
|
char ch = KB_CHARS[r][c];
|
|
|
|
|
|
if (caps && ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
|
|
|
|
|
|
char ch_buf[2] = { ch == ' ' ? '_' : ch, '\0' };
|
|
|
|
|
|
int cx = c * KB_CELL_W;
|
|
|
|
|
|
if (sel) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.fillRect(cx, y - 1, KB_CELL_W - 1, KB_CELL_H);
|
|
|
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
}
|
|
|
|
|
|
display.setCursor(cx + 3, y);
|
|
|
|
|
|
display.print(ch_buf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 21:09:37 +02:00
|
|
|
|
// special row: [^] [Sp] [Dl] [{}] [OK]
|
|
|
|
|
|
const char* spec[] = { "[^]", "[Sp]", "[Dl]", "[{}]", "[OK]" };
|
2026-05-13 18:22:41 +02:00
|
|
|
|
for (int i = 0; i < KB_SPECIAL; i++) {
|
|
|
|
|
|
bool sel = (row == KB_ROWS_CHAR && col == i);
|
|
|
|
|
|
bool active = (i == 0 && caps);
|
|
|
|
|
|
int sx = i * 25;
|
|
|
|
|
|
if (sel || active) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.fillRect(sx, KB_SPECIAL_Y - 1, 24, KB_CELL_H);
|
|
|
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
}
|
|
|
|
|
|
display.setCursor(sx + 1, KB_SPECIAL_Y);
|
|
|
|
|
|
display.print(spec[i]);
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// placeholder picker overlay
|
|
|
|
|
|
if (ph_mode) {
|
|
|
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
|
display.fillRect(20, 20, 88, 12 + KB_PH_COUNT * 10);
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.drawRect(20, 20, 88, 12 + KB_PH_COUNT * 10);
|
|
|
|
|
|
display.setCursor(24, 21);
|
|
|
|
|
|
display.print("Placeholder:");
|
|
|
|
|
|
display.fillRect(20, 30, 88, 1);
|
|
|
|
|
|
for (int i = 0; i < KB_PH_COUNT; i++) {
|
|
|
|
|
|
int py = 33 + i * 10;
|
|
|
|
|
|
if (i == ph_sel) {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
display.fillRect(21, py - 1, 86, 10);
|
|
|
|
|
|
display.setColor(DisplayDriver::DARK);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
}
|
|
|
|
|
|
display.setCursor(24, py);
|
|
|
|
|
|
display.print(KB_PH_LIST[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
display.setColor(DisplayDriver::LIGHT);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return 50;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Result handleInput(char c) {
|
|
|
|
|
|
// placeholder overlay consumes all input
|
|
|
|
|
|
if (ph_mode) {
|
|
|
|
|
|
if (c == KEY_UP && ph_sel > 0) { ph_sel--; return NONE; }
|
|
|
|
|
|
if (c == KEY_DOWN && ph_sel < KB_PH_COUNT - 1){ ph_sel++; return NONE; }
|
|
|
|
|
|
if (c == KEY_ENTER) {
|
|
|
|
|
|
const char* ph = KB_PH_LIST[ph_sel];
|
|
|
|
|
|
int ph_len = strlen(ph);
|
|
|
|
|
|
if (len + ph_len <= max_len) {
|
|
|
|
|
|
memcpy(buf + len, ph, ph_len);
|
|
|
|
|
|
len += ph_len;
|
|
|
|
|
|
buf[len] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
ph_mode = false;
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
ph_mode = false; // CANCEL or any other key closes the overlay
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 18:28:04 +02:00
|
|
|
|
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) return CANCELLED;
|
2026-05-13 18:22:41 +02:00
|
|
|
|
|
|
|
|
|
|
if (c == KEY_UP) {
|
|
|
|
|
|
if (row > 0) {
|
|
|
|
|
|
row--;
|
|
|
|
|
|
if (row == KB_ROWS_CHAR - 1) // leaving special row upward
|
|
|
|
|
|
col = col * KB_COLS_CHAR / KB_SPECIAL;
|
|
|
|
|
|
}
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_DOWN) {
|
|
|
|
|
|
if (row < KB_ROWS_CHAR) {
|
|
|
|
|
|
row++;
|
|
|
|
|
|
if (row == KB_ROWS_CHAR) // entering special row
|
|
|
|
|
|
col = col * KB_SPECIAL / KB_COLS_CHAR;
|
|
|
|
|
|
}
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_LEFT) {
|
|
|
|
|
|
if (col > 0) col--;
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_RIGHT) {
|
|
|
|
|
|
int max_col = (row == KB_ROWS_CHAR) ? KB_SPECIAL - 1 : KB_COLS_CHAR - 1;
|
|
|
|
|
|
if (col < max_col) col++;
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c == KEY_ENTER) {
|
|
|
|
|
|
if (row < KB_ROWS_CHAR) {
|
|
|
|
|
|
if (len < max_len) {
|
|
|
|
|
|
char ch = KB_CHARS[row][col];
|
|
|
|
|
|
if (caps && ch >= 'a' && ch <= 'z') ch = ch - 'a' + 'A';
|
|
|
|
|
|
buf[len++] = ch;
|
|
|
|
|
|
buf[len] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
switch (col) {
|
|
|
|
|
|
case 0: caps = !caps; break;
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
if (len < max_len) { buf[len++] = ' '; buf[len] = '\0'; }
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
if (len > 0) buf[--len] = '\0';
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 3:
|
|
|
|
|
|
ph_mode = true;
|
|
|
|
|
|
ph_sel = 0;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 4:
|
|
|
|
|
|
return DONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return NONE;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|