mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
First limited emoji support!
This commit is contained in:
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
@@ -1,4 +1,6 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"pioarduino.pioarduino-ide",
|
||||
"platformio.platformio-ide"
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 8
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "9 Feb 2026"
|
||||
#define FIRMWARE_BUILD_DATE "10 Feb 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v0.7.3"
|
||||
#define FIRMWARE_VERSION "Meck v0.8"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
|
||||
@@ -15,19 +15,25 @@
|
||||
|
||||
// Compose mode state
|
||||
static bool composeMode = false;
|
||||
static char composeBuffer[138]; // 137 chars max + null terminator
|
||||
static int composePos = 0;
|
||||
static uint8_t composeChannelIdx = 0; // Which channel to send to
|
||||
static char composeBuffer[138]; // 137 bytes max + null terminator (matches BLE wire cost)
|
||||
static int composePos = 0; // Current wire-cost byte count
|
||||
static uint8_t composeChannelIdx = 0;
|
||||
static unsigned long lastComposeRefresh = 0;
|
||||
static bool composeNeedsRefresh = false;
|
||||
#define COMPOSE_REFRESH_INTERVAL 600 // ms between e-ink refreshes while typing (refresh takes ~650ms)
|
||||
|
||||
// Emoji picker state
|
||||
#include "EmojiPicker.h"
|
||||
static bool emojiPickerMode = false;
|
||||
static EmojiPicker emojiPicker;
|
||||
|
||||
// Text reader mode state
|
||||
static bool readerMode = false;
|
||||
|
||||
void initKeyboard();
|
||||
void handleKeyboardInput();
|
||||
void drawComposeScreen();
|
||||
void drawEmojiPicker();
|
||||
void sendComposedMessage();
|
||||
#endif
|
||||
|
||||
@@ -376,9 +382,13 @@ void loop() {
|
||||
if (!composeMode) {
|
||||
ui_task.loop();
|
||||
} else {
|
||||
// Handle debounced compose screen refresh
|
||||
// Handle debounced compose/emoji picker screen refresh
|
||||
if (composeNeedsRefresh && (millis() - lastComposeRefresh) >= COMPOSE_REFRESH_INTERVAL) {
|
||||
drawComposeScreen();
|
||||
if (emojiPickerMode) {
|
||||
drawEmojiPicker();
|
||||
} else {
|
||||
drawComposeScreen();
|
||||
}
|
||||
lastComposeRefresh = millis();
|
||||
composeNeedsRefresh = false;
|
||||
}
|
||||
@@ -427,6 +437,37 @@ void handleKeyboardInput() {
|
||||
key >= 32 ? key : '?', key, composeMode);
|
||||
|
||||
if (composeMode) {
|
||||
// Emoji picker sub-mode
|
||||
if (emojiPickerMode) {
|
||||
uint8_t result = emojiPicker.handleInput(key);
|
||||
if (result == 0xFF) {
|
||||
// Cancelled - immediate draw to return to compose
|
||||
emojiPickerMode = false;
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
composeNeedsRefresh = false;
|
||||
} else if (result >= EMOJI_ESCAPE_START && result <= EMOJI_ESCAPE_END) {
|
||||
// Emoji selected - insert escape byte + padding to match UTF-8 wire cost
|
||||
int cost = emojiUtf8Cost(result);
|
||||
if (composePos + cost <= 137) {
|
||||
composeBuffer[composePos++] = (char)result;
|
||||
for (int p = 1; p < cost; p++) {
|
||||
composeBuffer[composePos++] = (char)EMOJI_PAD_BYTE;
|
||||
}
|
||||
composeBuffer[composePos] = '\0';
|
||||
Serial.printf("Compose: Inserted emoji 0x%02X cost=%d, pos=%d\n", result, cost, composePos);
|
||||
}
|
||||
emojiPickerMode = false;
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
composeNeedsRefresh = false;
|
||||
} else {
|
||||
// Navigation - debounce (don't draw immediately, let loop handle it)
|
||||
composeNeedsRefresh = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// In compose mode - handle text input
|
||||
if (key == '\r') {
|
||||
// Enter - send the message
|
||||
@@ -435,6 +476,7 @@ void handleKeyboardInput() {
|
||||
sendComposedMessage();
|
||||
}
|
||||
composeMode = false;
|
||||
emojiPickerMode = false;
|
||||
composeBuffer[0] = '\0';
|
||||
composePos = 0;
|
||||
ui_task.gotoHomeScreen();
|
||||
@@ -447,16 +489,24 @@ void handleKeyboardInput() {
|
||||
// Shift+Backspace = Cancel (works anytime)
|
||||
Serial.println("Compose: Shift+Backspace, cancelling...");
|
||||
composeMode = false;
|
||||
emojiPickerMode = false;
|
||||
composeBuffer[0] = '\0';
|
||||
composePos = 0;
|
||||
ui_task.gotoHomeScreen();
|
||||
return;
|
||||
}
|
||||
// Regular backspace - delete last character
|
||||
// Regular backspace - delete last character (or entire emoji including pads)
|
||||
if (composePos > 0) {
|
||||
composePos--;
|
||||
// Delete trailing pad bytes first, then the escape byte
|
||||
while (composePos > 0 && (uint8_t)composeBuffer[composePos - 1] == EMOJI_PAD_BYTE) {
|
||||
composePos--;
|
||||
}
|
||||
// Now delete the actual character (escape byte or regular char)
|
||||
if (composePos > 0) {
|
||||
composePos--;
|
||||
}
|
||||
composeBuffer[composePos] = '\0';
|
||||
Serial.printf("Compose: Backspace, pos now %d\n", composePos);
|
||||
Serial.printf("Compose: Backspace, pos=%d\n", composePos);
|
||||
composeNeedsRefresh = true; // Use debounced refresh
|
||||
}
|
||||
return;
|
||||
@@ -478,7 +528,7 @@ void handleKeyboardInput() {
|
||||
}
|
||||
}
|
||||
Serial.printf("Compose: Channel switched to %d\n", composeChannelIdx);
|
||||
drawComposeScreen();
|
||||
composeNeedsRefresh = true; // Debounced refresh
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -492,7 +542,17 @@ void handleKeyboardInput() {
|
||||
composeChannelIdx = 0; // Wrap to first channel
|
||||
}
|
||||
Serial.printf("Compose: Channel switched to %d\n", composeChannelIdx);
|
||||
drawComposeScreen();
|
||||
composeNeedsRefresh = true; // Debounced refresh
|
||||
return;
|
||||
}
|
||||
|
||||
// '$' opens emoji picker
|
||||
if (key == '$') {
|
||||
emojiPicker.reset();
|
||||
emojiPickerMode = true;
|
||||
drawEmojiPicker();
|
||||
lastComposeRefresh = millis();
|
||||
composeNeedsRefresh = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -500,7 +560,7 @@ void handleKeyboardInput() {
|
||||
if (key >= 32 && key < 127 && composePos < 137) {
|
||||
composeBuffer[composePos++] = key;
|
||||
composeBuffer[composePos] = '\0';
|
||||
Serial.printf("Compose: Added '%c', pos now %d\n", key, composePos);
|
||||
Serial.printf("Compose: Added '%c', pos=%d\n", key, composePos);
|
||||
composeNeedsRefresh = true; // Use debounced refresh
|
||||
}
|
||||
return;
|
||||
@@ -532,6 +592,7 @@ void handleKeyboardInput() {
|
||||
composePos = 0;
|
||||
Serial.printf("Entering compose mode from reader, channel %d\n", composeChannelIdx);
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -554,6 +615,7 @@ void handleKeyboardInput() {
|
||||
}
|
||||
Serial.printf("Entering compose mode, channel %d\n", composeChannelIdx);
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
break;
|
||||
|
||||
case 'm':
|
||||
@@ -664,22 +726,42 @@ void drawComposeScreen() {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
// Word wrap the compose buffer - calculate chars per line based on actual font width
|
||||
int x = 0;
|
||||
int y = 14;
|
||||
uint16_t testWidth = display.getTextWidth("MMMMMMMMMM"); // 10 wide chars
|
||||
int charsPerLine = (testWidth > 0) ? (display.width() * 10) / testWidth : 20;
|
||||
if (charsPerLine < 12) charsPerLine = 12;
|
||||
if (charsPerLine > 40) charsPerLine = 40;
|
||||
char charStr[2] = {0, 0}; // Buffer for single character as string
|
||||
|
||||
// Track position in pixels for tight emoji placement
|
||||
int px = 0; // Current pixel X position
|
||||
int lineW = display.width();
|
||||
|
||||
for (int i = 0; i < composePos; i++) {
|
||||
charStr[0] = composeBuffer[i];
|
||||
display.print(charStr);
|
||||
x++;
|
||||
if (x >= charsPerLine) {
|
||||
x = 0;
|
||||
y += 11;
|
||||
display.setCursor(0, y);
|
||||
uint8_t b = (uint8_t)composeBuffer[i];
|
||||
|
||||
// Skip emoji padding bytes (used to match wire cost)
|
||||
if (b == EMOJI_PAD_BYTE) continue;
|
||||
|
||||
if (isEmojiEscape(b)) {
|
||||
// Check if emoji fits on this line
|
||||
if (px + EMOJI_LG_W > lineW) {
|
||||
px = 0;
|
||||
y += 11;
|
||||
display.setCursor(0, y);
|
||||
}
|
||||
const uint8_t* sprite = getEmojiSpriteLg(b);
|
||||
if (sprite) {
|
||||
display.drawXbm(px, y - 1, sprite, EMOJI_LG_W, EMOJI_LG_H);
|
||||
}
|
||||
px += EMOJI_LG_W + 1; // sprite width + 1px gap
|
||||
display.setCursor(px, y);
|
||||
} else {
|
||||
charStr[0] = (char)b;
|
||||
int cw = display.getTextWidth(charStr);
|
||||
if (px + cw > lineW) {
|
||||
px = 0;
|
||||
y += 11;
|
||||
display.setCursor(0, y);
|
||||
}
|
||||
display.print(charStr);
|
||||
px += cw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,13 +778,13 @@ void drawComposeScreen() {
|
||||
char status[40];
|
||||
if (composePos == 0) {
|
||||
// Empty buffer - show channel switching hint
|
||||
display.print("A/D:Ch");
|
||||
display.print("A/D:Ch $:Emoji");
|
||||
sprintf(status, "Sh+Del:X");
|
||||
display.setCursor(display.width() - display.getTextWidth(status) - 2, statusY);
|
||||
display.print(status);
|
||||
} else {
|
||||
// Has text - show send/cancel hint
|
||||
sprintf(status, "%d/137 Ent:Send", composePos);
|
||||
sprintf(status, "%d/137 $:Emj", composePos);
|
||||
display.print(status);
|
||||
sprintf(status, "Sh+Del:X");
|
||||
display.setCursor(display.width() - display.getTextWidth(status) - 2, statusY);
|
||||
@@ -713,6 +795,14 @@ void drawComposeScreen() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void drawEmojiPicker() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
display.startFrame();
|
||||
emojiPicker.draw(display);
|
||||
display.endFrame();
|
||||
#endif
|
||||
}
|
||||
|
||||
void sendComposedMessage() {
|
||||
if (composePos == 0) return;
|
||||
|
||||
@@ -721,19 +811,25 @@ void sendComposedMessage() {
|
||||
if (the_mesh.getChannel(composeChannelIdx, channel)) {
|
||||
uint32_t timestamp = rtc_clock.getCurrentTime();
|
||||
|
||||
// Send to channel
|
||||
// Convert escape bytes back to UTF-8 for mesh transmission and BLE app
|
||||
// Worst case: each escape byte → 8 bytes UTF-8 (flag emoji), plus ASCII chars
|
||||
char utf8Buf[512];
|
||||
emojiUnescape(composeBuffer, utf8Buf, sizeof(utf8Buf));
|
||||
int utf8Len = strlen(utf8Buf);
|
||||
|
||||
// Send UTF-8 version to mesh (so other devices/apps see real emoji)
|
||||
if (the_mesh.sendGroupMessage(timestamp, channel.channel,
|
||||
the_mesh.getNodePrefs()->node_name,
|
||||
composeBuffer, composePos)) {
|
||||
// Add the sent message to local channel history so we can see what we sent
|
||||
utf8Buf, utf8Len)) {
|
||||
// Add escape-byte version to local display (renders as sprites)
|
||||
ui_task.addSentChannelMessage(composeChannelIdx,
|
||||
the_mesh.getNodePrefs()->node_name,
|
||||
composeBuffer);
|
||||
|
||||
// Queue message for BLE app sync (so sent messages appear in companion app)
|
||||
// Queue UTF-8 version for BLE app sync (so companion app shows real emoji)
|
||||
the_mesh.queueSentChannelMessage(composeChannelIdx, timestamp,
|
||||
the_mesh.getNodePrefs()->node_name,
|
||||
composeBuffer);
|
||||
utf8Buf);
|
||||
|
||||
ui_task.showAlert("Sent!", 1500);
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ChannelDetails.h>
|
||||
#include <MeshCore.h>
|
||||
#include "EmojiSprites.h"
|
||||
|
||||
// Maximum messages to store in history
|
||||
#define CHANNEL_MSG_HISTORY_SIZE 20
|
||||
@@ -59,9 +60,9 @@ public:
|
||||
msg->channel_idx = channel_idx;
|
||||
msg->valid = true;
|
||||
|
||||
// The text already contains "Sender: message" format, just store it
|
||||
strncpy(msg->text, text, CHANNEL_MSG_TEXT_LEN - 1);
|
||||
msg->text[CHANNEL_MSG_TEXT_LEN - 1] = '\0';
|
||||
// Sanitize emoji: replace UTF-8 emoji sequences with single-byte escape codes
|
||||
// The text already contains "Sender: message" format
|
||||
emojiSanitize(text, msg->text, CHANNEL_MSG_TEXT_LEN);
|
||||
|
||||
if (_msgCount < CHANNEL_MSG_HISTORY_SIZE) {
|
||||
_msgCount++;
|
||||
@@ -132,12 +133,6 @@ public:
|
||||
// This ensures rendered glyphs (which extend lineHeight below y) stay above the footer
|
||||
int maxY = display.height() - footerHeight;
|
||||
|
||||
// Calculate chars per line based on actual font width
|
||||
uint16_t testWidth = display.getTextWidth("MMMMMMMMMM"); // 10 wide chars
|
||||
int charsPerLine = (testWidth > 0) ? (display.width() * 10) / testWidth : 35;
|
||||
if (charsPerLine < 20) charsPerLine = 20; // Minimum reasonable
|
||||
if (charsPerLine > 60) charsPerLine = 60; // Maximum reasonable
|
||||
|
||||
int y = headerHeight;
|
||||
|
||||
// Build list of messages for this channel (newest first)
|
||||
@@ -175,39 +170,63 @@ public:
|
||||
sprintf(tmp, "(%d) %dd ", msg->path_len == 0xFF ? 0 : msg->path_len, age / 86400);
|
||||
}
|
||||
display.print(tmp);
|
||||
int prefixLen = strlen(tmp); // Characters used by timestamp on first line
|
||||
// DO NOT advance y - message text continues on the same line
|
||||
|
||||
// Message text with character wrapping (continues after timestamp on first line)
|
||||
// Message text with character wrapping and inline emoji support
|
||||
// (continues after timestamp on first line)
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
int textLen = strlen(msg->text);
|
||||
int pos = 0;
|
||||
int linesForThisMsg = 0;
|
||||
int maxLinesPerMsg = 8;
|
||||
int x = prefixLen; // First line starts after the timestamp prefix
|
||||
char charStr[2] = {0, 0};
|
||||
|
||||
// Track position in pixels for tight emoji placement
|
||||
int lineW = display.width();
|
||||
int px = display.getTextWidth(tmp); // Pixel X after timestamp
|
||||
|
||||
// Cursor already positioned after timestamp print - don't reset it
|
||||
|
||||
while (pos < textLen && linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) {
|
||||
charStr[0] = msg->text[pos];
|
||||
display.print(charStr);
|
||||
x++;
|
||||
pos++;
|
||||
uint8_t b = (uint8_t)msg->text[pos];
|
||||
|
||||
if (x >= charsPerLine) {
|
||||
x = 0;
|
||||
linesForThisMsg++;
|
||||
y += lineHeight;
|
||||
if (linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) {
|
||||
if (isEmojiEscape(b)) {
|
||||
// Check if emoji fits on this line
|
||||
if (px + EMOJI_SM_W > lineW) {
|
||||
px = 0;
|
||||
linesForThisMsg++;
|
||||
y += lineHeight;
|
||||
if (linesForThisMsg >= maxLinesPerMsg || y + lineHeight > maxY) break;
|
||||
display.setCursor(0, y);
|
||||
}
|
||||
|
||||
const uint8_t* sprite = getEmojiSpriteSm(b);
|
||||
if (sprite) {
|
||||
display.drawXbm(px, y - 1, sprite, EMOJI_SM_W, EMOJI_SM_H);
|
||||
}
|
||||
pos++;
|
||||
px += EMOJI_SM_W + 1; // sprite width + 1px gap
|
||||
display.setCursor(px, y);
|
||||
} else {
|
||||
charStr[0] = (char)b;
|
||||
int cw = display.getTextWidth(charStr);
|
||||
if (px + cw > lineW) {
|
||||
px = 0;
|
||||
linesForThisMsg++;
|
||||
y += lineHeight;
|
||||
if (linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) {
|
||||
display.setCursor(0, y);
|
||||
} else break;
|
||||
}
|
||||
display.print(charStr);
|
||||
px += cw;
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't end on a full line, still count it
|
||||
if (x > 0) {
|
||||
if (px > 0) {
|
||||
y += lineHeight;
|
||||
}
|
||||
|
||||
|
||||
410
examples/companion_radio/ui-new/Emojisprites.h
Normal file
410
examples/companion_radio/ui-new/Emojisprites.h
Normal file
@@ -0,0 +1,410 @@
|
||||
#pragma once
|
||||
|
||||
// Emoji sprites for e-ink display - dual size
|
||||
// Large (12x12) for compose/picker, Small (10x10) for channel view
|
||||
// MSB-first, 2 bytes per row
|
||||
|
||||
#include <stdint.h>
|
||||
#ifdef ESP32
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
// Large sprites for compose screen and picker
|
||||
#define EMOJI_LG_W 12
|
||||
#define EMOJI_LG_H 12
|
||||
|
||||
// Small sprites for channel message view
|
||||
#define EMOJI_SM_W 10
|
||||
#define EMOJI_SM_H 10
|
||||
|
||||
#define EMOJI_COUNT 20
|
||||
|
||||
// Escape codes used in sanitized message text
|
||||
// Bytes 0x01 through 0x14 map to emoji indices 0-19
|
||||
#define EMOJI_ESCAPE_START 0x01
|
||||
#define EMOJI_ESCAPE_END 0x14
|
||||
#define EMOJI_PAD_BYTE 0x15 // Padding byte after escape to fill buffer to UTF-8 wire cost
|
||||
|
||||
// ======== LARGE 12x12 SPRITES ========
|
||||
|
||||
static const uint8_t emoji_lg_wireless[] PROGMEM = {
|
||||
0x00,0x00, 0x3F,0xC0, 0x60,0x60, 0xC0,0x30,
|
||||
0x0F,0x00, 0x19,0x80, 0x30,0xC0, 0x00,0x00,
|
||||
0x06,0x00, 0x0F,0x00, 0x06,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_infinity[] PROGMEM = {
|
||||
0x00,0x00, 0x00,0x00, 0x61,0x80, 0x92,0x40,
|
||||
0x8C,0x40, 0x8C,0x40, 0x92,0x40, 0x61,0x80,
|
||||
0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_trex[] PROGMEM = {
|
||||
0x03,0xE0, 0x06,0xA0, 0x07,0xE0, 0x0C,0x00,
|
||||
0x5C,0x00, 0x7C,0x00, 0x3C,0x00, 0x38,0x00,
|
||||
0x3C,0x00, 0x36,0x00, 0x22,0x00, 0x33,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_skull[] PROGMEM = {
|
||||
0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0,
|
||||
0x40,0x20, 0x49,0x20, 0x2F,0x40, 0x1F,0x80,
|
||||
0x96,0x90, 0x66,0x60, 0x36,0xC0, 0x96,0x90,
|
||||
};
|
||||
static const uint8_t emoji_lg_cross[] PROGMEM = {
|
||||
0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x3F,0xC0,
|
||||
0x3F,0xC0, 0x3F,0xC0, 0x0F,0x00, 0x0F,0x00,
|
||||
0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_lightning[] PROGMEM = {
|
||||
0x03,0x00, 0x07,0x00, 0x0E,0x00, 0x1C,0x00,
|
||||
0x3F,0x80, 0x01,0x80, 0x03,0x00, 0x06,0x00,
|
||||
0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_tophat[] PROGMEM = {
|
||||
0x00,0x00, 0x1F,0x80, 0x3F,0xC0, 0x3F,0xC0,
|
||||
0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0,
|
||||
0x7F,0xE0, 0xFF,0xF0, 0xFF,0xF0, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_motorcycle[] PROGMEM = {
|
||||
0x00,0x00, 0x00,0x00, 0x03,0x80, 0x1F,0xC0,
|
||||
0x3F,0xC0, 0x7F,0xC0, 0xFF,0xE0, 0xDF,0x60,
|
||||
0x51,0x40, 0xE0,0xE0, 0x40,0x40, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_seedling[] PROGMEM = {
|
||||
0x00,0x00, 0x30,0x00, 0x79,0x80, 0x7B,0xC0,
|
||||
0x33,0xC0, 0x1F,0x80, 0x06,0x00, 0x06,0x00,
|
||||
0x06,0x00, 0x06,0x00, 0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_flag_au[] PROGMEM = {
|
||||
0x00,0x00, 0x32,0x40, 0x4A,0x40, 0x4A,0x40,
|
||||
0x7A,0x40, 0x4A,0x40, 0x49,0x80, 0x00,0x00,
|
||||
0xFF,0xF0, 0x00,0x00, 0xFF,0xF0, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_umbrella[] PROGMEM = {
|
||||
0x06,0x00, 0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0,
|
||||
0xFF,0xF0, 0xDB,0x70, 0x06,0x00, 0x06,0x00,
|
||||
0x06,0x00, 0x06,0x00, 0x46,0x00, 0x3C,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_nazar[] PROGMEM = {
|
||||
0x1F,0x80, 0x20,0x40, 0x4F,0x20, 0x99,0x90,
|
||||
0xB6,0xD0, 0xB6,0xD0, 0xB6,0xD0, 0x99,0x90,
|
||||
0x4F,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_globe[] PROGMEM = {
|
||||
0x1F,0x80, 0x34,0xC0, 0x66,0x60, 0x4F,0x20,
|
||||
0x8E,0x10, 0x86,0x10, 0x80,0x30, 0x46,0x60,
|
||||
0x43,0xE0, 0x30,0xC0, 0x1F,0x80, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_radioactive[] PROGMEM = {
|
||||
0x00,0x00, 0x22,0x40, 0x32,0xC0, 0x32,0xC0,
|
||||
0x1B,0x40, 0x00,0x00, 0x0F,0x00, 0x0F,0x00,
|
||||
0x00,0x00, 0x60,0x20, 0x39,0xC0, 0x0F,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_cow[] PROGMEM = {
|
||||
0x00,0x00, 0xC0,0x60, 0x6E,0xC0, 0x3F,0x80,
|
||||
0x2A,0x80, 0x3F,0x80, 0x3F,0x80, 0x7F,0xC0,
|
||||
0x5F,0x40, 0x5F,0x40, 0x11,0x00, 0x31,0x80,
|
||||
};
|
||||
static const uint8_t emoji_lg_alien[] PROGMEM = {
|
||||
0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0x76,0xE0,
|
||||
0xF6,0xF0, 0x96,0x90, 0x7F,0xE0, 0x36,0xC0,
|
||||
0x3F,0xC0, 0x16,0x80, 0x0F,0x00, 0x06,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_invader[] PROGMEM = {
|
||||
0x10,0x80, 0x09,0x00, 0x1F,0x80, 0x36,0xC0,
|
||||
0x7F,0xE0, 0x5F,0xA0, 0x50,0xA0, 0x50,0xA0,
|
||||
0x19,0x80, 0x19,0x80, 0x30,0xC0, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_dagger[] PROGMEM = {
|
||||
0x00,0x20, 0x00,0x60, 0x00,0xC0, 0x01,0x80,
|
||||
0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00,
|
||||
0x38,0x00, 0x58,0x00, 0x28,0x00, 0x18,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_grimace[] PROGMEM = {
|
||||
0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0,
|
||||
0x40,0x20, 0x40,0x20, 0x5F,0xA0, 0x55,0x40,
|
||||
0x5F,0xA0, 0x20,0x40, 0x1F,0x80, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_lg_telephone[] PROGMEM = {
|
||||
0x00,0x00, 0x7F,0xE0, 0xC0,0x30, 0xC0,0x30,
|
||||
0x60,0x60, 0x30,0xC0, 0x1F,0x80, 0x0F,0x00,
|
||||
0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0x00,0x00,
|
||||
};
|
||||
|
||||
static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = {
|
||||
emoji_lg_wireless, emoji_lg_infinity, emoji_lg_trex, emoji_lg_skull, emoji_lg_cross,
|
||||
emoji_lg_lightning, emoji_lg_tophat, emoji_lg_motorcycle, emoji_lg_seedling, emoji_lg_flag_au,
|
||||
emoji_lg_umbrella, emoji_lg_nazar, emoji_lg_globe, emoji_lg_radioactive, emoji_lg_cow,
|
||||
emoji_lg_alien, emoji_lg_invader, emoji_lg_dagger, emoji_lg_grimace, emoji_lg_telephone,
|
||||
};
|
||||
|
||||
// ======== SMALL 10x10 SPRITES ========
|
||||
|
||||
static const uint8_t emoji_sm_wireless[] PROGMEM = {
|
||||
0x00,0x00, 0x7F,0x80, 0xC0,0xC0, 0x1E,0x00,
|
||||
0x33,0x00, 0x21,0x00, 0x00,0x00, 0x0C,0x00,
|
||||
0x0C,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_infinity[] PROGMEM = {
|
||||
0x00,0x00, 0x00,0x00, 0xE7,0x00, 0x99,0x00,
|
||||
0x99,0x00, 0xA5,0x00, 0x42,0x00, 0x00,0x00,
|
||||
0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_trex[] PROGMEM = {
|
||||
0x07,0x80, 0x0F,0x80, 0x0F,0x80, 0x58,0x00,
|
||||
0x78,0x00, 0x38,0x00, 0x38,0x00, 0x3C,0x00,
|
||||
0x24,0x00, 0x26,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_skull[] PROGMEM = {
|
||||
0x3F,0x00, 0x61,0x80, 0x73,0x80, 0x40,0x80,
|
||||
0x52,0x80, 0x3F,0x00, 0x3F,0x00, 0xED,0xC0,
|
||||
0x6D,0x80, 0xAD,0x40,
|
||||
};
|
||||
static const uint8_t emoji_sm_cross[] PROGMEM = {
|
||||
0x1E,0x00, 0x1E,0x00, 0x3F,0x00, 0x3F,0x00,
|
||||
0x3F,0x00, 0x1E,0x00, 0x1E,0x00, 0x1E,0x00,
|
||||
0x1E,0x00, 0x1E,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_lightning[] PROGMEM = {
|
||||
0x06,0x00, 0x0E,0x00, 0x1C,0x00, 0x3E,0x00,
|
||||
0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00,
|
||||
0x30,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_tophat[] PROGMEM = {
|
||||
0x00,0x00, 0x3F,0x00, 0x3F,0x00, 0x3F,0x00,
|
||||
0x3F,0x00, 0x3F,0x00, 0x7F,0x80, 0xFF,0xC0,
|
||||
0xFF,0xC0, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_motorcycle[] PROGMEM = {
|
||||
0x00,0x00, 0x00,0x00, 0x1F,0x00, 0x3F,0x00,
|
||||
0x7F,0x00, 0xFF,0x80, 0xFF,0x80, 0xE3,0x80,
|
||||
0xC1,0x80, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_seedling[] PROGMEM = {
|
||||
0x00,0x00, 0x70,0x00, 0x77,0x00, 0x77,0x00,
|
||||
0x3F,0x00, 0x0C,0x00, 0x0C,0x00, 0x0C,0x00,
|
||||
0x00,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_flag_au[] PROGMEM = {
|
||||
0x00,0x00, 0x75,0x00, 0x55,0x00, 0x75,0x00,
|
||||
0x55,0x00, 0x53,0x00, 0x00,0x00, 0xFF,0xC0,
|
||||
0xFF,0xC0, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_umbrella[] PROGMEM = {
|
||||
0x0C,0x00, 0x3F,0x00, 0x7F,0x80, 0xFF,0xC0,
|
||||
0xF7,0xC0, 0x0C,0x00, 0x0C,0x00, 0x0C,0x00,
|
||||
0x4C,0x00, 0x78,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_nazar[] PROGMEM = {
|
||||
0x3F,0x00, 0x40,0x80, 0x9E,0x40, 0xBF,0x40,
|
||||
0xAD,0x40, 0xBF,0x40, 0x9E,0x40, 0x4C,0x80,
|
||||
0x3F,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_globe[] PROGMEM = {
|
||||
0x3F,0x00, 0x69,0x80, 0x4C,0x80, 0x9C,0x40,
|
||||
0x8C,0x40, 0x80,0xC0, 0x4D,0x80, 0x67,0x80,
|
||||
0x3F,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_radioactive[] PROGMEM = {
|
||||
0x00,0x00, 0x25,0x00, 0x25,0x00, 0x37,0x00,
|
||||
0x00,0x00, 0x1E,0x00, 0x1E,0x00, 0x40,0x00,
|
||||
0x73,0x80, 0x1E,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_cow[] PROGMEM = {
|
||||
0x00,0x00, 0xC1,0x80, 0x7F,0x00, 0x3F,0x00,
|
||||
0x3F,0x00, 0x7F,0x00, 0x7F,0x00, 0x7F,0x00,
|
||||
0x36,0x00, 0x23,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_alien[] PROGMEM = {
|
||||
0x3F,0x00, 0x7F,0x80, 0x7F,0x80, 0xED,0xC0,
|
||||
0xAD,0x40, 0x7F,0x80, 0x3F,0x00, 0x3F,0x00,
|
||||
0x1E,0x00, 0x0C,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_invader[] PROGMEM = {
|
||||
0x33,0x00, 0x1E,0x00, 0x3F,0x00, 0x7F,0x80,
|
||||
0x7F,0x80, 0x61,0x80, 0x73,0x80, 0x33,0x00,
|
||||
0x33,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_dagger[] PROGMEM = {
|
||||
0x00,0x80, 0x01,0x80, 0x03,0x00, 0x06,0x00,
|
||||
0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x70,0x00,
|
||||
0x70,0x00, 0x30,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_grimace[] PROGMEM = {
|
||||
0x3F,0x00, 0x61,0x80, 0x73,0x80, 0x40,0x80,
|
||||
0x40,0x80, 0x7F,0x80, 0x55,0x00, 0x7F,0x80,
|
||||
0x3F,0x00, 0x00,0x00,
|
||||
};
|
||||
static const uint8_t emoji_sm_telephone[] PROGMEM = {
|
||||
0x00,0x00, 0xFF,0xC0, 0xC0,0xC0, 0xC0,0xC0,
|
||||
0x61,0x80, 0x3F,0x00, 0x1E,0x00, 0x3F,0x00,
|
||||
0x7F,0x80, 0x00,0x00,
|
||||
};
|
||||
|
||||
static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = {
|
||||
emoji_sm_wireless, emoji_sm_infinity, emoji_sm_trex, emoji_sm_skull, emoji_sm_cross,
|
||||
emoji_sm_lightning, emoji_sm_tophat, emoji_sm_motorcycle, emoji_sm_seedling, emoji_sm_flag_au,
|
||||
emoji_sm_umbrella, emoji_sm_nazar, emoji_sm_globe, emoji_sm_radioactive, emoji_sm_cow,
|
||||
emoji_sm_alien, emoji_sm_invader, emoji_sm_dagger, emoji_sm_grimace, emoji_sm_telephone,
|
||||
};
|
||||
|
||||
// ---- Codepoint lookup for UTF-8 detection ----
|
||||
struct EmojiCodepoint {
|
||||
uint32_t cp;
|
||||
uint32_t cp2;
|
||||
uint8_t escape;
|
||||
};
|
||||
|
||||
static const EmojiCodepoint EMOJI_CODEPOINTS[20] = {
|
||||
{ 0x1F6DC, 0x0000, 0x01 }, { 0x267E, 0x0000, 0x02 }, { 0x1F996, 0x0000, 0x03 },
|
||||
{ 0x2620, 0x0000, 0x04 }, { 0x271D, 0x0000, 0x05 }, { 0x26A1, 0x0000, 0x06 },
|
||||
{ 0x1F3A9, 0x0000, 0x07 }, { 0x1F3CD, 0x0000, 0x08 }, { 0x1F331, 0x0000, 0x09 },
|
||||
{ 0x1F1E6, 0x1F1FA, 0x0A }, { 0x2602, 0x0000, 0x0B }, { 0x1F9FF, 0x0000, 0x0C },
|
||||
{ 0x1F30F, 0x0000, 0x0D }, { 0x2622, 0x0000, 0x0E }, { 0x1F404, 0x0000, 0x0F },
|
||||
{ 0x1F47D, 0x0000, 0x10 }, { 0x1F47E, 0x0000, 0x11 }, { 0x1F5E1, 0x0000, 0x12 },
|
||||
{ 0x1F62C, 0x0000, 0x13 }, { 0x260E, 0x0000, 0x14 },
|
||||
};
|
||||
|
||||
// ---- Helper functions ----
|
||||
|
||||
static uint32_t emojiDecodeUtf8(const uint8_t* s, int remaining, int* bytes_consumed) {
|
||||
uint8_t b0 = s[0];
|
||||
if (b0 < 0x80) { *bytes_consumed = 1; return b0; }
|
||||
if ((b0 & 0xE0) == 0xC0 && remaining >= 2) {
|
||||
*bytes_consumed = 2;
|
||||
return ((uint32_t)(b0 & 0x1F) << 6) | (s[1] & 0x3F);
|
||||
}
|
||||
if ((b0 & 0xF0) == 0xE0 && remaining >= 3) {
|
||||
*bytes_consumed = 3;
|
||||
return ((uint32_t)(b0 & 0x0F) << 12) | ((uint32_t)(s[1] & 0x3F) << 6) | (s[2] & 0x3F);
|
||||
}
|
||||
if ((b0 & 0xF8) == 0xF0 && remaining >= 4) {
|
||||
*bytes_consumed = 4;
|
||||
return ((uint32_t)(b0 & 0x07) << 18) | ((uint32_t)(s[1] & 0x3F) << 12) | ((uint32_t)(s[2] & 0x3F) << 6) | (s[3] & 0x3F);
|
||||
}
|
||||
*bytes_consumed = 1;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
static void emojiSanitize(const char* src, char* dst, int dstLen) {
|
||||
const uint8_t* s = (const uint8_t*)src;
|
||||
int si = 0, di = 0;
|
||||
int srcLen = strlen(src);
|
||||
while (si < srcLen && di < dstLen - 1) {
|
||||
uint8_t b = s[si];
|
||||
if (b >= 0xE0) {
|
||||
int consumed;
|
||||
uint32_t cp = emojiDecodeUtf8(s + si, srcLen - si, &consumed);
|
||||
if (cp == 0xFE0F) { si += consumed; continue; }
|
||||
bool found = false;
|
||||
for (int e = 0; e < 20; e++) {
|
||||
if (EMOJI_CODEPOINTS[e].cp == cp) {
|
||||
if (EMOJI_CODEPOINTS[e].cp2 != 0) {
|
||||
int consumed2;
|
||||
if (si + consumed < srcLen) {
|
||||
uint32_t cp2 = emojiDecodeUtf8(s + si + consumed, srcLen - si - consumed, &consumed2);
|
||||
if (cp2 == EMOJI_CODEPOINTS[e].cp2) {
|
||||
dst[di++] = EMOJI_CODEPOINTS[e].escape;
|
||||
si += consumed + consumed2;
|
||||
found = true; break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
dst[di++] = EMOJI_CODEPOINTS[e].escape;
|
||||
si += consumed;
|
||||
if (si + 2 < srcLen && s[si] == 0xEF && s[si+1] == 0xB8 && s[si+2] == 0x8F) si += 3;
|
||||
found = true; break;
|
||||
}
|
||||
}
|
||||
if (!found) si += consumed;
|
||||
} else {
|
||||
dst[di++] = (char)b;
|
||||
si++;
|
||||
}
|
||||
}
|
||||
dst[di] = '\0';
|
||||
}
|
||||
|
||||
static inline bool isEmojiEscape(uint8_t b) {
|
||||
return b >= EMOJI_ESCAPE_START && b <= EMOJI_ESCAPE_END;
|
||||
}
|
||||
|
||||
// Encode a Unicode codepoint as UTF-8 into dst, return bytes written
|
||||
static int emojiEncodeUtf8(uint32_t cp, uint8_t* dst) {
|
||||
if (cp < 0x80) {
|
||||
dst[0] = (uint8_t)cp;
|
||||
return 1;
|
||||
} else if (cp < 0x800) {
|
||||
dst[0] = 0xC0 | (cp >> 6);
|
||||
dst[1] = 0x80 | (cp & 0x3F);
|
||||
return 2;
|
||||
} else if (cp < 0x10000) {
|
||||
dst[0] = 0xE0 | (cp >> 12);
|
||||
dst[1] = 0x80 | ((cp >> 6) & 0x3F);
|
||||
dst[2] = 0x80 | (cp & 0x3F);
|
||||
return 3;
|
||||
} else {
|
||||
dst[0] = 0xF0 | (cp >> 18);
|
||||
dst[1] = 0x80 | ((cp >> 12) & 0x3F);
|
||||
dst[2] = 0x80 | ((cp >> 6) & 0x3F);
|
||||
dst[3] = 0x80 | (cp & 0x3F);
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse of emojiSanitize: convert escape bytes back to UTF-8 emoji sequences
|
||||
// Used before sending over mesh/BLE so other devices and apps see real emoji
|
||||
// dst must be large enough (worst case: srcLen * 8 for all flag emoji)
|
||||
static void emojiUnescape(const char* src, char* dst, int dstLen) {
|
||||
int si = 0, di = 0;
|
||||
int srcLen = strlen(src);
|
||||
while (si < srcLen && di < dstLen - 1) {
|
||||
uint8_t b = (uint8_t)src[si];
|
||||
if (b == EMOJI_PAD_BYTE) {
|
||||
si++; // Skip padding bytes
|
||||
continue;
|
||||
}
|
||||
if (isEmojiEscape(b)) {
|
||||
int idx = b - EMOJI_ESCAPE_START;
|
||||
if (idx < 20) {
|
||||
uint8_t utf8[8];
|
||||
int len = emojiEncodeUtf8(EMOJI_CODEPOINTS[idx].cp, utf8);
|
||||
// Encode second codepoint if present (flag sequences)
|
||||
if (EMOJI_CODEPOINTS[idx].cp2 != 0) {
|
||||
len += emojiEncodeUtf8(EMOJI_CODEPOINTS[idx].cp2, utf8 + len);
|
||||
}
|
||||
if (di + len < dstLen) {
|
||||
memcpy(dst + di, utf8, len);
|
||||
di += len;
|
||||
} else break; // No room
|
||||
}
|
||||
si++;
|
||||
} else {
|
||||
dst[di++] = src[si++];
|
||||
}
|
||||
}
|
||||
dst[di] = '\0';
|
||||
}
|
||||
|
||||
// Get large sprite (for compose/picker)
|
||||
static inline const uint8_t* getEmojiSpriteLg(uint8_t escape_byte) {
|
||||
if (!isEmojiEscape(escape_byte)) return nullptr;
|
||||
return (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_LG[escape_byte - EMOJI_ESCAPE_START]);
|
||||
}
|
||||
|
||||
// Get small sprite (for channel view)
|
||||
static inline const uint8_t* getEmojiSpriteSm(uint8_t escape_byte) {
|
||||
if (!isEmojiEscape(escape_byte)) return nullptr;
|
||||
return (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_SM[escape_byte - EMOJI_ESCAPE_START]);
|
||||
}
|
||||
|
||||
// Get the UTF-8 wire cost in bytes for an escape byte (4 for most emoji, 8 for flags)
|
||||
static inline int emojiUtf8Cost(uint8_t escape_byte) {
|
||||
if (!isEmojiEscape(escape_byte)) return 1; // not an emoji, regular char
|
||||
int idx = escape_byte - EMOJI_ESCAPE_START;
|
||||
uint32_t cp = EMOJI_CODEPOINTS[idx].cp;
|
||||
int cost = (cp < 0x80) ? 1 : (cp < 0x800) ? 2 : (cp < 0x10000) ? 3 : 4;
|
||||
if (EMOJI_CODEPOINTS[idx].cp2 != 0) {
|
||||
uint32_t cp2 = EMOJI_CODEPOINTS[idx].cp2;
|
||||
cost += (cp2 < 0x80) ? 1 : (cp2 < 0x800) ? 2 : (cp2 < 0x10000) ? 3 : 4;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
135
examples/companion_radio/ui-new/emojipicker.h
Normal file
135
examples/companion_radio/ui-new/emojipicker.h
Normal file
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
// Emoji Picker for compose mode
|
||||
// Shows a grid of available emoji sprites, navigable with WASD + Enter to select
|
||||
// Requires EmojiSprites.h to be included before this file
|
||||
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include "EmojiSprites.h"
|
||||
|
||||
// Grid layout: 5 columns x 4 rows = 20 emojis
|
||||
#define EMOJI_PICKER_COLS 5
|
||||
#define EMOJI_PICKER_ROWS 4
|
||||
|
||||
// Short labels shown alongside sprites for identification
|
||||
static const char* EMOJI_LABELS[EMOJI_COUNT] = {
|
||||
"WiFi", // 0 🛜
|
||||
"Inf", // 1 ♾️
|
||||
"Rex", // 2 🦖
|
||||
"Skul", // 3 ☠️
|
||||
"Cros", // 4 ✝️
|
||||
"Bolt", // 5 ⚡
|
||||
"Hat", // 6 🎩
|
||||
"Moto", // 7 🏍️
|
||||
"Leaf", // 8 🌱
|
||||
"AU", // 9 🇦🇺
|
||||
"Umbr", // 10 ☂️
|
||||
"Eye", // 11 🧿
|
||||
"Glob", // 12 🌏
|
||||
"Rad", // 13 ☢️
|
||||
"Cow", // 14 🐄
|
||||
"ET", // 15 👽
|
||||
"Inv", // 16 👾
|
||||
"Dagr", // 17 🗡️
|
||||
"Grim", // 18 😬
|
||||
"Fone", // 19 ☎️
|
||||
};
|
||||
|
||||
struct EmojiPicker {
|
||||
int cursor; // 0 to EMOJI_COUNT-1
|
||||
|
||||
EmojiPicker() : cursor(0) {}
|
||||
|
||||
void reset() { cursor = 0; }
|
||||
|
||||
// Returns the emoji escape byte for the selected emoji, or 0 if cancelled
|
||||
// Navigate: W=up, S=down, A=left, D=right, Enter=select, Backspace/Q/$=cancel
|
||||
uint8_t handleInput(char key) {
|
||||
int col = cursor % EMOJI_PICKER_COLS;
|
||||
int row = cursor / EMOJI_PICKER_COLS;
|
||||
|
||||
switch (key) {
|
||||
case 'w': case 'W': case 0xF2: // Up
|
||||
if (row > 0) cursor -= EMOJI_PICKER_COLS;
|
||||
return 0;
|
||||
case 's': case 'S': case 0xF1: // Down
|
||||
if (row < EMOJI_PICKER_ROWS - 1) cursor += EMOJI_PICKER_COLS;
|
||||
return 0;
|
||||
case 'a': case 'A': // Left
|
||||
if (col > 0) cursor--;
|
||||
else if (row > 0) cursor -= 1; // Wrap to end of previous row
|
||||
return 0;
|
||||
case 'd': case 'D': // Right
|
||||
if (cursor + 1 < EMOJI_COUNT) cursor++;
|
||||
return 0;
|
||||
case '\r': // Enter - select
|
||||
return (uint8_t)(EMOJI_ESCAPE_START + cursor);
|
||||
case '\b': case 'q': case 'Q': case '$': // Cancel
|
||||
return 0xFF; // Sentinel for "cancelled"
|
||||
default:
|
||||
return 0; // No action
|
||||
}
|
||||
}
|
||||
|
||||
void draw(DisplayDriver& display) {
|
||||
// Header
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, 0);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.print("Select Emoji");
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// Grid area
|
||||
display.setTextSize(0); // Tiny font for labels
|
||||
|
||||
int startY = 14;
|
||||
int cellW = display.width() / EMOJI_PICKER_COLS;
|
||||
int cellH = (display.height() - startY - 14) / EMOJI_PICKER_ROWS; // Leave room for footer
|
||||
|
||||
for (int i = 0; i < EMOJI_COUNT; i++) {
|
||||
int col = i % EMOJI_PICKER_COLS;
|
||||
int row = i / EMOJI_PICKER_COLS;
|
||||
int cx = col * cellW;
|
||||
int cy = startY + row * cellH;
|
||||
|
||||
// Draw selection border around highlighted cell
|
||||
if (i == cursor) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(cx, cy, cellW, cellH);
|
||||
display.drawRect(cx + 1, cy + 1, cellW - 2, cellH - 2); // Double border for visibility
|
||||
}
|
||||
|
||||
// Draw sprite centered in cell
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
const uint8_t* sprite = (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_LG[i]);
|
||||
if (sprite) {
|
||||
int spriteX = cx + (cellW - EMOJI_LG_W) / 2;
|
||||
int spriteY = cy + 1;
|
||||
display.drawXbm(spriteX, spriteY, sprite, EMOJI_LG_W, EMOJI_LG_H);
|
||||
}
|
||||
|
||||
// Label below sprite
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
// Center label text in cell
|
||||
uint16_t labelW = display.getTextWidth(EMOJI_LABELS[i]);
|
||||
int labelX = cx + (cellW * 7 - labelW) / (7 * 2); // Approximate centering
|
||||
display.setCursor(labelX, cy + 12);
|
||||
display.print(EMOJI_LABELS[i]);
|
||||
}
|
||||
|
||||
// Footer
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setCursor(0, footerY);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.print("WASD:Nav Ent:Pick");
|
||||
|
||||
const char* cancelText = "$:Back";
|
||||
display.setCursor(display.width() - display.getTextWidth(cancelText) - 2, footerY);
|
||||
display.print(cancelText);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user