diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b1d3ecf9..df570003 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -725,47 +725,81 @@ void drawComposeScreen() { display.setCursor(0, 14); display.setColor(DisplayDriver::LIGHT); - // Word wrap the compose buffer - calculate chars per line based on actual font width + // Word wrap the compose buffer with word-boundary awareness + // Uses advance width (cursor movement) not bounding box width for px tracking. + // Advance = getTextWidth("cc") - getTextWidth("c") to get true cursor step. int y = 14; - char charStr[2] = {0, 0}; // Buffer for single character as string + char charStr[2] = {0, 0}; + char dblStr[3] = {0, 0, 0}; - // Track position in pixels for tight emoji placement - int px = 0; // Current pixel X position + int px = 0; int lineW = display.width(); + bool atWordBoundary = true; for (int i = 0; i < composePos; i++) { uint8_t b = (uint8_t)composeBuffer[i]; - // Skip emoji padding bytes (used to match wire cost) if (b == EMOJI_PAD_BYTE) continue; + // Word wrap: when starting a new text word, check if it fits on this line + if (atWordBoundary && b != ' ' && !isEmojiEscape(b) && px > 0) { + int wordW = 0; + for (int j = i; j < composePos; j++) { + uint8_t wb = (uint8_t)composeBuffer[j]; + if (wb == EMOJI_PAD_BYTE) continue; + if (wb == ' ' || isEmojiEscape(wb)) break; + dblStr[0] = dblStr[1] = (char)wb; + charStr[0] = (char)wb; + wordW += display.getTextWidth(dblStr) - display.getTextWidth(charStr); + } + if (px + wordW > lineW) { + px = 0; + y += 12; + } + } + if (isEmojiEscape(b)) { - // Check if emoji fits on this line if (px + EMOJI_LG_W > lineW) { px = 0; - y += 11; - display.setCursor(0, y); + y += 12; } const uint8_t* sprite = getEmojiSpriteLg(b); if (sprite) { - display.drawXbm(px, y - 1, sprite, EMOJI_LG_W, EMOJI_LG_H); + display.drawXbm(px, y, sprite, EMOJI_LG_W, EMOJI_LG_H); } - px += EMOJI_LG_W + 1; // sprite width + 1px gap + px += EMOJI_LG_W + 1; display.setCursor(px, y); + atWordBoundary = true; + } else if (b == ' ') { + charStr[0] = ' '; + dblStr[0] = dblStr[1] = ' '; + int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr); + if (px + adv > lineW) { + px = 0; + y += 12; + } else { + display.setCursor(px, y); + display.print(charStr); + px += adv; + } + atWordBoundary = true; } else { charStr[0] = (char)b; - int cw = display.getTextWidth(charStr); - if (px + cw > lineW) { + dblStr[0] = dblStr[1] = (char)b; + int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr); + if (px + adv > lineW) { px = 0; - y += 11; - display.setCursor(0, y); + y += 12; } + display.setCursor(px, y); display.print(charStr); - px += cw; + px += adv; + atWordBoundary = false; } } // Show cursor + display.setCursor(px, y); display.print("_"); // Status bar diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index f727ddc4..5137cb07 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -182,45 +182,93 @@ public: int maxLinesPerMsg = 8; char charStr[2] = {0, 0}; - // Track position in pixels for tight emoji placement + // Track position in pixels for emoji placement + // Uses advance width (cursor movement) not bounding box for px tracking int lineW = display.width(); int px = display.getTextWidth(tmp); // Pixel X after timestamp - - // Cursor already positioned after timestamp print - don't reset it + char dblStr[3] = {0, 0, 0}; while (pos < textLen && linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) { uint8_t b = (uint8_t)msg->text[pos]; + if (b == EMOJI_PAD_BYTE) { pos++; continue; } + + // Word wrap: when starting a new text word, check if it fits + if (b != ' ' && !isEmojiEscape(b) && px > 0) { + bool boundary = (pos == 0); + if (!boundary) { + for (int bp = pos - 1; bp >= 0; bp--) { + uint8_t pb = (uint8_t)msg->text[bp]; + if (pb == EMOJI_PAD_BYTE) continue; + boundary = (pb == ' ' || isEmojiEscape(pb)); + break; + } + } + if (boundary) { + int wordW = 0; + for (int j = pos; j < textLen; j++) { + uint8_t wb = (uint8_t)msg->text[j]; + if (wb == EMOJI_PAD_BYTE) continue; + if (wb == ' ' || isEmojiEscape(wb)) break; + charStr[0] = (char)wb; + dblStr[0] = dblStr[1] = (char)wb; + wordW += display.getTextWidth(dblStr) - display.getTextWidth(charStr); + } + if (px + wordW > lineW) { + px = 0; + linesForThisMsg++; + y += lineHeight; + if (linesForThisMsg >= maxLinesPerMsg || y + lineHeight > maxY) break; + } + } + } + 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); + display.drawXbm(px, y, sprite, EMOJI_SM_W, EMOJI_SM_H); } pos++; - px += EMOJI_SM_W + 1; // sprite width + 1px gap + px += EMOJI_SM_W + 1; display.setCursor(px, y); - } else { - charStr[0] = (char)b; - int cw = display.getTextWidth(charStr); - if (px + cw > lineW) { + } else if (b == ' ') { + charStr[0] = ' '; + dblStr[0] = dblStr[1] = ' '; + int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr); + if (px + adv > lineW) { px = 0; linesForThisMsg++; y += lineHeight; if (linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) { - display.setCursor(0, y); + // skip trailing space at wrap + } else break; + } else { + display.setCursor(px, y); + display.print(charStr); + px += adv; + } + pos++; + } else { + charStr[0] = (char)b; + dblStr[0] = dblStr[1] = (char)b; + int adv = display.getTextWidth(dblStr) - display.getTextWidth(charStr); + if (px + adv > lineW) { + px = 0; + linesForThisMsg++; + y += lineHeight; + if (linesForThisMsg < maxLinesPerMsg && y + lineHeight <= maxY) { + // continue to print below } else break; } + display.setCursor(px, y); display.print(charStr); - px += cw; + px += adv; pos++; } } diff --git a/examples/companion_radio/ui-new/Emojisprites.h b/examples/companion_radio/ui-new/Emojisprites.h index 8ce25e75..34f7cd3e 100644 --- a/examples/companion_radio/ui-new/Emojisprites.h +++ b/examples/companion_radio/ui-new/Emojisprites.h @@ -3,263 +3,437 @@ // Emoji sprites for e-ink display - dual size // Large (12x12) for compose/picker, Small (10x10) for channel view // MSB-first, 2 bytes per row +// 47 total emoji: joy/thumbsup/frown first, then 44 original #include #ifdef ESP32 #include #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 +#define EMOJI_COUNT 47 -// 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 +// Escape codes in 0x80+ range - safe from keyboard ASCII (32-126) +#define EMOJI_ESCAPE_START 0x80 +#define EMOJI_ESCAPE_END 0xAE // 0x80 + 46 +#define EMOJI_PAD_BYTE 0x7F // DEL, not typeable (key < 127 guard) // ======== LARGE 12x12 SPRITES ======== +// [0] joy (most common mesh emoji) +static const uint8_t emoji_lg_joy[] PROGMEM = { + 0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0, 0x80,0x10, 0xA0,0x50, 0x9F,0x90, 0x40,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00, 0x00,0x00, +}; +// [1] thumbsup +static const uint8_t emoji_lg_thumbsup[] PROGMEM = { + 0x00,0x00, 0x70,0x00, 0x70,0x00, 0x70,0x00, 0x7F,0x80, 0xFF,0x80, 0xFF,0x80, 0x7F,0x80, 0x3F,0x80, 0x1F,0x00, 0x00,0x00, 0x00,0x00, +}; +// [2] frown +static const uint8_t emoji_lg_frown[] PROGMEM = { + 0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0, 0x80,0x10, 0x9F,0x90, 0xA0,0x50, 0x40,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00, 0x00,0x00, +}; +// [3] wireless 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, + 0x00,0x00, 0x3F,0xC0, 0x60,0x60, 0xC0,0x30, 0x0F,0x00, 0x19,0x80, 0x30,0xC0, 0x00,0x00, 0x06,0x00, 0x0F,0x00, 0x06,0x00, 0x00,0x00, }; +// [4] infinity 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, + 0x00,0x00, 0x00,0x00, 0x61,0x80, 0x92,0x40, 0x8C,0x40, 0x8C,0x40, 0x92,0x40, 0x61,0x80, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00, }; +// [5] trex 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, + 0x03,0xE0, 0x06,0xA0, 0x07,0xE0, 0x0C,0x00, 0x5C,0x00, 0x7C,0x00, 0x3C,0x00, 0x38,0x00, 0x3C,0x00, 0x36,0x00, 0x22,0x00, 0x33,0x00, }; +// [6] skull 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, + 0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0, 0x40,0x20, 0x49,0x20, 0x2F,0x40, 0x1F,0x80, 0x96,0x90, 0x66,0x60, 0x36,0xC0, 0x96,0x90, }; +// [7] cross 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, + 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, 0x0F,0x00, }; +// [8] lightning 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, + 0x03,0x00, 0x07,0x00, 0x0E,0x00, 0x1C,0x00, 0x3F,0x80, 0x01,0x80, 0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x00,0x00, }; +// [9] tophat 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, + 0x00,0x00, 0x1F,0x80, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x3F,0xC0, 0x20,0x40, 0x3F,0xC0, 0x7F,0xE0, 0xFF,0xF0, 0xFF,0xF0, 0x00,0x00, }; +// [10] motorcycle 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, + 0x00,0x00, 0x00,0x00, 0x0F,0x00, 0x1F,0x80, 0x7F,0xE0, 0xDF,0xB0, 0xD0,0xB0, 0xD0,0xB0, 0xDF,0xB0, 0x60,0x60, 0x00,0x00, 0x00,0x00, }; +// [11] seedling 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, + 0x00,0x00, 0x30,0x00, 0x79,0x80, 0x7B,0xC0, 0x33,0xC0, 0x1F,0x80, 0x06,0x00, 0x06,0x00, 0x06,0x00, 0x06,0x00, 0x00,0x00, 0x00,0x00, }; +// [12] flag_au 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, + 0x00,0x00, 0x32,0x40, 0x4A,0x40, 0x4A,0x40, 0x7A,0x40, 0x4A,0x40, 0x49,0x80, 0x00,0x00, 0xFF,0xF0, 0x00,0x00, 0xFF,0xF0, 0x00,0x00, }; +// [13] umbrella 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, + 0x06,0x00, 0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0xFF,0xF0, 0xDB,0x70, 0x06,0x00, 0x06,0x00, 0x06,0x00, 0x06,0x00, 0x46,0x00, 0x3C,0x00, }; +// [14] nazar 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, + 0x1F,0x80, 0x20,0x40, 0x4F,0x20, 0x99,0x90, 0xB6,0xD0, 0xB6,0xD0, 0xB6,0xD0, 0x99,0x90, 0x4F,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00, }; +// [15] globe 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, + 0x1F,0x80, 0x34,0xC0, 0x66,0x60, 0x4F,0x20, 0x8E,0x10, 0x86,0x10, 0x80,0x30, 0x46,0x60, 0x43,0xE0, 0x30,0xC0, 0x1F,0x80, 0x00,0x00, }; +// [16] radioactive 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, + 0x00,0x00, 0x22,0x40, 0x32,0xC0, 0x32,0xC0, 0x1B,0x40, 0x00,0x00, 0x0F,0x00, 0x0F,0x00, 0x00,0x00, 0x60,0x20, 0x39,0xC0, 0x0F,0x00, }; +// [17] cow 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, + 0x00,0x00, 0xC0,0x60, 0x6E,0xC0, 0x3F,0x80, 0x2A,0x80, 0x3F,0x80, 0x3F,0x80, 0x7F,0xC0, 0x5F,0x40, 0x5F,0x40, 0x11,0x00, 0x31,0x80, }; +// [18] alien 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, + 0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0x76,0xE0, 0xF6,0xF0, 0x96,0x90, 0x7F,0xE0, 0x36,0xC0, 0x3F,0xC0, 0x16,0x80, 0x0F,0x00, 0x06,0x00, }; +// [19] invader 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, + 0x10,0x80, 0x09,0x00, 0x1F,0x80, 0x36,0xC0, 0x7F,0xE0, 0x5F,0xA0, 0x50,0xA0, 0x50,0xA0, 0x19,0x80, 0x19,0x80, 0x30,0xC0, 0x00,0x00, }; +// [20] dagger 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, + 0x00,0x20, 0x00,0x60, 0x00,0xC0, 0x01,0x80, 0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00, 0x38,0x00, 0x58,0x00, 0x28,0x00, 0x18,0x00, }; +// [21] grimace 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, + 0x1F,0x80, 0x20,0x40, 0x59,0xA0, 0x59,0xA0, 0x40,0x20, 0x40,0x20, 0x5F,0xA0, 0x55,0x40, 0x5F,0xA0, 0x20,0x40, 0x1F,0x80, 0x00,0x00, }; +// [22] telephone 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, + 0x00,0x00, 0x7F,0xE0, 0xC0,0x30, 0xC0,0x30, 0x60,0x60, 0x30,0xC0, 0x1F,0x80, 0x0F,0x00, 0x1F,0x80, 0x3F,0xC0, 0x7F,0xE0, 0x00,0x00, +}; +// [23] mountain +static const uint8_t emoji_lg_mountain[] PROGMEM = { + 0x00,0x00, 0x00,0x00, 0x06,0x00, 0x0F,0x00, 0x19,0x80, 0x30,0xC0, 0x66,0x60, 0xCF,0x30, 0x9F,0x90, 0xFF,0xF0, 0xFF,0xF0, 0x00,0x00, +}; +// [24] end_arrow +static const uint8_t emoji_lg_end_arrow[] PROGMEM = { + 0x00,0x00, 0x7B,0x60, 0x43,0x60, 0x42,0xA0, 0x72,0xA0, 0x43,0x60, 0x43,0x60, 0x7B,0x60, 0x00,0x00, 0x06,0x00, 0x0F,0x00, 0x06,0x00, +}; +// [25] hollow_circle +static const uint8_t emoji_lg_hollow_circle[] PROGMEM = { + 0x1F,0x80, 0x20,0x40, 0x40,0x20, 0x80,0x10, 0x80,0x10, 0x80,0x10, 0x80,0x10, 0x80,0x10, 0x40,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00, +}; +// [26] dragon +static const uint8_t emoji_lg_dragon[] PROGMEM = { + 0x60,0x00, 0xF0,0x00, 0x76,0x00, 0x3F,0x00, 0x1F,0x00, 0x0F,0x00, 0x1F,0x80, 0x3F,0xC0, 0x79,0xE0, 0x30,0xC0, 0x20,0x40, 0x30,0xC0, +}; +// [27] globe_meridians +static const uint8_t emoji_lg_globe_meridians[] PROGMEM = { + 0x1F,0x80, 0x26,0x40, 0x46,0x20, 0x86,0x10, 0xFF,0xF0, 0x86,0x10, 0x86,0x10, 0x46,0x20, 0x26,0x40, 0x1F,0x80, 0x00,0x00, 0x00,0x00, +}; +// [28] eggplant +static const uint8_t emoji_lg_eggplant[] PROGMEM = { + 0x01,0x80, 0x03,0x00, 0x07,0x00, 0x0F,0x00, 0x1F,0x00, 0x3F,0x00, 0x3F,0x00, 0x7E,0x00, 0x7C,0x00, 0x78,0x00, 0x30,0x00, 0x00,0x00, +}; +// [29] shield +static const uint8_t emoji_lg_shield[] PROGMEM = { + 0x00,0x00, 0x7F,0xE0, 0x7F,0xE0, 0x6F,0x60, 0x6F,0x60, 0x6F,0x60, 0x36,0xC0, 0x3F,0xC0, 0x1F,0x80, 0x0F,0x00, 0x06,0x00, 0x00,0x00, +}; +// [30] goggles +static const uint8_t emoji_lg_goggles[] PROGMEM = { + 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x79,0xE0, 0xCF,0x30, 0x86,0x10, 0x86,0x10, 0xCF,0x30, 0x79,0xE0, 0x00,0x00, 0x00,0x00, 0x00,0x00, +}; +// [31] lizard +static const uint8_t emoji_lg_lizard[] PROGMEM = { + 0x00,0x60, 0x00,0xF0, 0x01,0xB0, 0x03,0x00, 0x06,0x00, 0x7C,0x00, 0xC0,0x00, 0x60,0x00, 0x30,0x00, 0x18,0x00, 0x0E,0x00, 0x06,0x00, +}; +// [32] zany_face +static const uint8_t emoji_lg_zany_face[] PROGMEM = { + 0x1F,0x80, 0x20,0x40, 0x59,0x20, 0x58,0xA0, 0x40,0x20, 0x40,0x20, 0x4F,0x20, 0x50,0xA0, 0x20,0x40, 0x1F,0x80, 0x00,0x00, 0x00,0x00, +}; +// [33] kangaroo +static const uint8_t emoji_lg_kangaroo[] PROGMEM = { + 0x0E,0x00, 0x1F,0x00, 0x1F,0x00, 0x0E,0x00, 0x0F,0x00, 0x07,0x80, 0x47,0x80, 0x65,0x80, 0x3C,0x80, 0x18,0x80, 0x10,0xC0, 0x18,0xF0, +}; +// [34] feather +static const uint8_t emoji_lg_feather[] PROGMEM = { + 0x00,0x20, 0x00,0x60, 0x00,0xC0, 0x01,0x80, 0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x60,0x00, 0x70,0x00, 0x00,0x00, +}; +// [35] bright +static const uint8_t emoji_lg_bright[] PROGMEM = { + 0x06,0x00, 0x26,0x40, 0x16,0x80, 0x0F,0x00, 0x6F,0x60, 0x6F,0x60, 0x0F,0x00, 0x16,0x80, 0x26,0x40, 0x06,0x00, 0x00,0x00, 0x00,0x00, +}; +// [36] part_alt +static const uint8_t emoji_lg_part_alt[] PROGMEM = { + 0x60,0xC0, 0x71,0xC0, 0x3B,0x80, 0x1F,0x00, 0x0E,0x00, 0x0E,0x00, 0x1F,0x00, 0x3B,0x80, 0x71,0xC0, 0x60,0xC0, 0x00,0x00, 0x00,0x00, +}; +// [37] motorboat +static const uint8_t emoji_lg_motorboat[] PROGMEM = { + 0x00,0x00, 0x00,0x00, 0x02,0x00, 0x07,0x00, 0x0F,0x80, 0x1F,0xC0, 0xFF,0xF0, 0x7F,0xE0, 0x3F,0xC0, 0x1F,0x80, 0x00,0x00, 0x00,0x00, +}; +// [38] playing_card +static const uint8_t emoji_lg_playing_card[] PROGMEM = { + 0x7F,0x80, 0x40,0x80, 0x4C,0x80, 0x52,0x80, 0x52,0x80, 0x52,0x80, 0x4C,0x80, 0x40,0x80, 0x7F,0x80, 0x00,0x00, 0x00,0x00, 0x00,0x00, +}; +// [39] satellite +static const uint8_t emoji_lg_satellite[] PROGMEM = { + 0x78,0x00, 0xCC,0x00, 0x84,0x00, 0xCD,0x00, 0x7B,0x00, 0x03,0x80, 0x01,0xC0, 0x00,0xE0, 0x00,0x60, 0x00,0x20, 0x00,0x00, 0x00,0x00, +}; +// [40] customs +static const uint8_t emoji_lg_customs[] PROGMEM = { + 0x1F,0x80, 0x20,0x40, 0x40,0x20, 0x4F,0x20, 0x50,0xA0, 0x50,0xA0, 0x4F,0x20, 0x42,0x20, 0x22,0x40, 0x1F,0x80, 0x00,0x00, 0x00,0x00, +}; +// [41] cowboy +static const uint8_t emoji_lg_cowboy[] PROGMEM = { + 0x0F,0x00, 0x0F,0x00, 0x7F,0xE0, 0xFF,0xF0, 0x00,0x00, 0x3F,0xC0, 0x59,0xA0, 0x40,0x20, 0x4F,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00, +}; +// [42] wheel +static const uint8_t emoji_lg_wheel[] PROGMEM = { + 0x1F,0x80, 0x26,0x40, 0x46,0x20, 0x9F,0x90, 0xB6,0xD0, 0xFF,0xF0, 0xB6,0xD0, 0x9F,0x90, 0x46,0x20, 0x26,0x40, 0x1F,0x80, 0x00,0x00, +}; +// [43] koala +static const uint8_t emoji_lg_koala[] PROGMEM = { + 0x60,0x60, 0xF0,0xF0, 0xF0,0xF0, 0x76,0xE0, 0x26,0x40, 0x2F,0x40, 0x26,0x40, 0x30,0xC0, 0x1F,0x80, 0x00,0x00, 0x00,0x00, 0x00,0x00, +}; +// [44] control_knobs +static const uint8_t emoji_lg_control_knobs[] PROGMEM = { + 0x00,0x00, 0x33,0x30, 0x33,0x30, 0x33,0x30, 0x33,0x30, 0x33,0x30, 0x33,0x30, 0x7B,0x30, 0x37,0xB0, 0x33,0x70, 0x33,0x30, 0x00,0x00, +}; +// [45] peach +static const uint8_t emoji_lg_peach[] PROGMEM = { + 0x06,0x00, 0x0C,0x00, 0x1E,0x00, 0x3F,0x00, 0x7F,0x80, 0x7B,0xC0, 0x7B,0xC0, 0x7B,0xC0, 0x3F,0xC0, 0x1F,0x80, 0x0F,0x00, 0x00,0x00, +}; +// [46] racing_car +static const uint8_t emoji_lg_racing_car[] PROGMEM = { + 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x07,0x80, 0x0F,0xC0, 0x7F,0xE0, 0xFF,0xF0, 0xFF,0xF0, 0x6F,0x60, 0x49,0x20, 0x00,0x00, 0x00,0x00, }; static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = { + emoji_lg_joy, emoji_lg_thumbsup, emoji_lg_frown, 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, + emoji_lg_mountain, emoji_lg_end_arrow, emoji_lg_hollow_circle, emoji_lg_dragon, emoji_lg_globe_meridians, + emoji_lg_eggplant, emoji_lg_shield, emoji_lg_goggles, emoji_lg_lizard, emoji_lg_zany_face, + emoji_lg_kangaroo, emoji_lg_feather, emoji_lg_bright, emoji_lg_part_alt, emoji_lg_motorboat, + emoji_lg_playing_card, emoji_lg_satellite, emoji_lg_customs, emoji_lg_cowboy, emoji_lg_wheel, + emoji_lg_koala, emoji_lg_control_knobs, emoji_lg_peach, emoji_lg_racing_car, }; // ======== SMALL 10x10 SPRITES ======== +static const uint8_t emoji_sm_joy[] PROGMEM = { + 0x3F,0x00, 0x61,0x80, 0xF3,0xC0, 0x80,0x40, 0xA1,0x40, 0x9E,0x40, 0x40,0x80, 0x3F,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_thumbsup[] PROGMEM = { + 0x70,0x00, 0x70,0x00, 0x70,0x00, 0x7F,0x00, 0xFF,0x00, 0xFF,0x00, 0x7F,0x00, 0x3E,0x00, 0x1C,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_frown[] PROGMEM = { + 0x3F,0x00, 0x61,0x80, 0xF3,0xC0, 0x80,0x40, 0x9E,0x40, 0xA1,0x40, 0x40,0x80, 0x3F,0x00, 0x00,0x00, 0x00,0x00, +}; 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, + 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, + 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, + 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, + 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, + 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, + 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, + 0x00,0x00, 0x3F,0x00, 0x3F,0x00, 0x3F,0x00, 0x21,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, + 0x00,0x00, 0x1E,0x00, 0x7F,0x80, 0xDE,0xC0, 0xD2,0xC0, 0xD2,0xC0, 0xDE,0xC0, 0x61,0x80, 0x00,0x00, 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 0x00,0x00, 0xFF,0xC0, 0xC0,0xC0, 0xC0,0xC0, 0x61,0x80, 0x3F,0x00, 0x1E,0x00, 0x3F,0x00, 0x7F,0x80, 0x00,0x00, +}; +static const uint8_t emoji_sm_mountain[] PROGMEM = { + 0x00,0x00, 0x00,0x00, 0x0C,0x00, 0x1E,0x00, 0x33,0x00, 0x6D,0x80, 0xDE,0xC0, 0xFF,0xC0, 0xFF,0xC0, 0x00,0x00, +}; +static const uint8_t emoji_sm_end_arrow[] PROGMEM = { + 0x00,0x00, 0x77,0x80, 0x47,0x80, 0x65,0x80, 0x47,0x80, 0x47,0x80, 0x76,0x80, 0x0C,0x00, 0x1E,0x00, 0x0C,0x00, +}; +static const uint8_t emoji_sm_hollow_circle[] PROGMEM = { + 0x3F,0x00, 0x40,0x80, 0x80,0x40, 0x80,0x40, 0x80,0x40, 0x80,0x40, 0x80,0x40, 0x40,0x80, 0x3F,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_dragon[] PROGMEM = { + 0x60,0x00, 0xE0,0x00, 0x7C,0x00, 0x3E,0x00, 0x1E,0x00, 0x3F,0x00, 0x7F,0x80, 0x73,0x80, 0x21,0x00, 0x21,0x00, +}; +static const uint8_t emoji_sm_globe_meridians[] PROGMEM = { + 0x3F,0x00, 0x4C,0x80, 0x8C,0x40, 0xFF,0xC0, 0x8C,0x40, 0x8C,0x40, 0x4C,0x80, 0x3F,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_eggplant[] PROGMEM = { + 0x03,0x00, 0x06,0x00, 0x0E,0x00, 0x1E,0x00, 0x3E,0x00, 0x7E,0x00, 0x7C,0x00, 0x78,0x00, 0x70,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_shield[] PROGMEM = { + 0x00,0x00, 0xFF,0xC0, 0xFF,0xC0, 0xDE,0xC0, 0xDE,0xC0, 0x6D,0x80, 0x7F,0x80, 0x3F,0x00, 0x1E,0x00, 0x0C,0x00, +}; +static const uint8_t emoji_sm_goggles[] PROGMEM = { + 0x00,0x00, 0x00,0x00, 0x73,0x80, 0xDE,0xC0, 0x8C,0x40, 0x8C,0x40, 0xDE,0xC0, 0x73,0x80, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_lizard[] PROGMEM = { + 0x01,0x80, 0x03,0xC0, 0x06,0x40, 0x0C,0x00, 0x78,0x00, 0xC0,0x00, 0x60,0x00, 0x30,0x00, 0x1C,0x00, 0x0C,0x00, +}; +static const uint8_t emoji_sm_zany_face[] PROGMEM = { + 0x3F,0x00, 0x60,0x80, 0x72,0x80, 0x40,0x80, 0x40,0x80, 0x5E,0x80, 0x61,0x80, 0x3F,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_kangaroo[] PROGMEM = { + 0x1C,0x00, 0x3E,0x00, 0x1C,0x00, 0x1E,0x00, 0x0F,0x00, 0x4F,0x00, 0x6B,0x00, 0x39,0x00, 0x31,0x00, 0x31,0xC0, +}; +static const uint8_t emoji_sm_feather[] PROGMEM = { + 0x00,0x80, 0x01,0x80, 0x03,0x00, 0x06,0x00, 0x0C,0x00, 0x18,0x00, 0x30,0x00, 0x60,0x00, 0x60,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_bright[] PROGMEM = { + 0x0C,0x00, 0x2D,0x00, 0x1E,0x00, 0x5E,0x80, 0x7F,0x80, 0x1E,0x00, 0x2D,0x00, 0x0C,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_part_alt[] PROGMEM = { + 0x63,0x00, 0x77,0x00, 0x3E,0x00, 0x1C,0x00, 0x1C,0x00, 0x3E,0x00, 0x77,0x00, 0x63,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_motorboat[] PROGMEM = { + 0x00,0x00, 0x00,0x00, 0x0C,0x00, 0x1E,0x00, 0x3F,0x00, 0xFF,0xC0, 0x7F,0x80, 0x3F,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_playing_card[] PROGMEM = { + 0x7F,0x00, 0x41,0x00, 0x5D,0x00, 0x65,0x00, 0x65,0x00, 0x59,0x00, 0x43,0x00, 0x7E,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_satellite[] PROGMEM = { + 0x70,0x00, 0xD8,0x00, 0x88,0x00, 0xFE,0x00, 0x07,0x00, 0x03,0x80, 0x01,0x80, 0x00,0x80, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_customs[] PROGMEM = { + 0x3F,0x00, 0x40,0x80, 0x4C,0x80, 0x52,0x80, 0x61,0x80, 0x5E,0x80, 0x44,0x80, 0x3F,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_cowboy[] PROGMEM = { + 0x1E,0x00, 0x1E,0x00, 0xFF,0xC0, 0x00,0x00, 0x3F,0x00, 0x73,0x80, 0x40,0x80, 0x4C,0x80, 0x3F,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_wheel[] PROGMEM = { + 0x3F,0x00, 0x4C,0x80, 0x9E,0x40, 0xBF,0x40, 0xFF,0xC0, 0xBF,0x40, 0x9E,0x40, 0x4C,0x80, 0x3F,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_koala[] PROGMEM = { + 0x61,0x80, 0xE1,0xC0, 0xED,0xC0, 0x6D,0x80, 0x3F,0x00, 0x2D,0x00, 0x33,0x00, 0x1E,0x00, 0x00,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_control_knobs[] PROGMEM = { + 0x00,0x00, 0x26,0xC0, 0x26,0xC0, 0x26,0xC0, 0x26,0xC0, 0x76,0xC0, 0x7E,0xC0, 0x2F,0xC0, 0x26,0xC0, 0x00,0x00, +}; +static const uint8_t emoji_sm_peach[] PROGMEM = { + 0x0C,0x00, 0x18,0x00, 0x3C,0x00, 0x7E,0x00, 0x77,0x00, 0x77,0x00, 0x7F,0x00, 0x3F,0x00, 0x1E,0x00, 0x00,0x00, +}; +static const uint8_t emoji_sm_racing_car[] PROGMEM = { + 0x00,0x00, 0x00,0x00, 0x0E,0x00, 0x1F,0x00, 0x7F,0x80, 0xFF,0xC0, 0xFF,0xC0, 0x5E,0x80, 0x00,0x00, 0x00,0x00, }; static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = { + emoji_sm_joy, emoji_sm_thumbsup, emoji_sm_frown, 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, + emoji_sm_mountain, emoji_sm_end_arrow, emoji_sm_hollow_circle, emoji_sm_dragon, emoji_sm_globe_meridians, + emoji_sm_eggplant, emoji_sm_shield, emoji_sm_goggles, emoji_sm_lizard, emoji_sm_zany_face, + emoji_sm_kangaroo, emoji_sm_feather, emoji_sm_bright, emoji_sm_part_alt, emoji_sm_motorboat, + emoji_sm_playing_card, emoji_sm_satellite, emoji_sm_customs, emoji_sm_cowboy, emoji_sm_wheel, + emoji_sm_koala, emoji_sm_control_knobs, emoji_sm_peach, emoji_sm_racing_car, }; -// ---- Codepoint lookup for UTF-8 detection ---- -struct EmojiCodepoint { - uint32_t cp; - uint32_t cp2; - uint8_t escape; -}; +// ---- Codepoint lookup for UTF-8 conversion ---- +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 }, +static const EmojiCodepoint EMOJI_CODEPOINTS[EMOJI_COUNT] = { + { 0x1F602, 0x0000, 0x80 }, // joy + { 0x1F44D, 0x0000, 0x81 }, // thumbsup + { 0x2639, 0x0000, 0x82 }, // frown + { 0x1F6DC, 0x0000, 0x83 }, // wireless + { 0x267E, 0x0000, 0x84 }, // infinity + { 0x1F996, 0x0000, 0x85 }, // trex + { 0x2620, 0x0000, 0x86 }, // skull + { 0x271D, 0x0000, 0x87 }, // cross + { 0x26A1, 0x0000, 0x88 }, // lightning + { 0x1F3A9, 0x0000, 0x89 }, // tophat + { 0x1F3CD, 0x0000, 0x8A }, // motorcycle + { 0x1F331, 0x0000, 0x8B }, // seedling + { 0x1F1E6, 0x1F1FA, 0x8C }, // flag_au + { 0x2602, 0x0000, 0x8D }, // umbrella + { 0x1F9FF, 0x0000, 0x8E }, // nazar + { 0x1F30F, 0x0000, 0x8F }, // globe + { 0x2622, 0x0000, 0x90 }, // radioactive + { 0x1F404, 0x0000, 0x91 }, // cow + { 0x1F47D, 0x0000, 0x92 }, // alien + { 0x1F47E, 0x0000, 0x93 }, // invader + { 0x1F5E1, 0x0000, 0x94 }, // dagger + { 0x1F62C, 0x0000, 0x95 }, // grimace + { 0x260E, 0x0000, 0x96 }, // telephone + { 0x26F0, 0x0000, 0x97 }, // mountain + { 0x1F51A, 0x0000, 0x98 }, // end_arrow + { 0x2B55, 0x0000, 0x99 }, // hollow_circle + { 0x1F409, 0x0000, 0x9A }, // dragon + { 0x1F310, 0x0000, 0x9B }, // globe_meridians + { 0x1F346, 0x0000, 0x9C }, // eggplant + { 0x1F6E1, 0x0000, 0x9D }, // shield + { 0x1F97D, 0x0000, 0x9E }, // goggles + { 0x1F98E, 0x0000, 0x9F }, // lizard + { 0x1F92A, 0x0000, 0xA0 }, // zany_face + { 0x1F998, 0x0000, 0xA1 }, // kangaroo + { 0x1FAB6, 0x0000, 0xA2 }, // feather + { 0x1F506, 0x0000, 0xA3 }, // bright + { 0x303D, 0x0000, 0xA4 }, // part_alt + { 0x1F6E5, 0x0000, 0xA5 }, // motorboat + { 0x1F0CE, 0x0000, 0xA6 }, // playing_card + { 0x1F4E1, 0x0000, 0xA7 }, // satellite + { 0x1F6C3, 0x0000, 0xA8 }, // customs + { 0x1F920, 0x0000, 0xA9 }, // cowboy + { 0x1F6DE, 0x0000, 0xAA }, // wheel + { 0x1F428, 0x0000, 0xAB }, // koala + { 0x1F39B, 0x0000, 0xAC }, // control_knobs + { 0x1F351, 0x0000, 0xAD }, // peach + { 0x1F3CE, 0x0000, 0xAE }, // racing_car }; // ---- Helper functions ---- @@ -283,18 +457,20 @@ static uint32_t emojiDecodeUtf8(const uint8_t* s, int remaining, int* bytes_cons return 0xFFFD; } +// Convert UTF-8 text to internal format (emoji codepoints -> escape bytes) +// Now handles ALL multi-byte UTF-8 (>= 0x80) to prevent raw high bytes in buffer 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) { + if (b >= 0x80) { 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++) { + for (int e = 0; e < EMOJI_COUNT; e++) { if (EMOJI_CODEPOINTS[e].cp == cp) { if (EMOJI_CODEPOINTS[e].cp2 != 0) { int consumed2; @@ -310,11 +486,12 @@ static void emojiSanitize(const char* src, char* dst, int dstLen) { } dst[di++] = EMOJI_CODEPOINTS[e].escape; si += consumed; + // Skip trailing variation selector U+FE0F if (si + 2 < srcLen && s[si] == 0xEF && s[si+1] == 0xB8 && s[si+2] == 0x8F) si += 3; found = true; break; } } - if (!found) si += consumed; + if (!found) si += consumed; // Skip unknown multi-byte chars } else { dst[di++] = (char)b; si++; @@ -327,78 +504,46 @@ 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; - } + if (cp < 0x80) { dst[0] = (uint8_t)cp; return 1; } + if (cp < 0x800) { dst[0] = 0xC0|(cp>>6); dst[1] = 0x80|(cp&0x3F); return 2; } + if (cp < 0x10000) { dst[0] = 0xE0|(cp>>12); dst[1] = 0x80|((cp>>6)&0x3F); dst[2] = 0x80|(cp&0x3F); return 3; } + 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 (b == EMOJI_PAD_BYTE) { si++; continue; } if (isEmojiEscape(b)) { int idx = b - EMOJI_ESCAPE_START; - if (idx < 20) { + if (idx < EMOJI_COUNT) { 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) { + 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 + if (di + len < dstLen) { memcpy(dst + di, utf8, len); di += len; } else break; } si++; - } else { - dst[di++] = src[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 + if (!isEmojiEscape(escape_byte)) return 1; 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; diff --git a/examples/companion_radio/ui-new/emojipicker.h b/examples/companion_radio/ui-new/emojipicker.h index c796394e..1562e744 100644 --- a/examples/companion_radio/ui-new/emojipicker.h +++ b/examples/companion_radio/ui-new/emojipicker.h @@ -1,78 +1,119 @@ #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 +// Emoji Picker with scrolling grid and scroll bar +// 5 columns, 4 visible rows, scrollable through all 47 emoji +// WASD navigation, Enter to select, $/Q/Backspace to cancel #include #include "EmojiSprites.h" -// Grid layout: 5 columns x 4 rows = 20 emojis #define EMOJI_PICKER_COLS 5 -#define EMOJI_PICKER_ROWS 4 +#define EMOJI_PICKER_VISIBLE_ROWS 4 +#define EMOJI_PICKER_TOTAL_ROWS ((EMOJI_COUNT + EMOJI_PICKER_COLS - 1) / EMOJI_PICKER_COLS) -// 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 ☎️ + "Lol", // 0 joy + "Like", // 1 thumbsup + "Sad", // 2 frown + "WiFi", // 3 wireless + "Inf", // 4 infinity + "Rex", // 5 trex + "Skul", // 6 skull + "Cros", // 7 cross + "Bolt", // 8 lightning + "Hat", // 9 tophat + "Moto", // 10 motorcycle + "Leaf", // 11 seedling + "AU", // 12 flag_au + "Umbr", // 13 umbrella + "Eye", // 14 nazar + "Glob", // 15 globe + "Rad", // 16 radioactive + "Cow", // 17 cow + "ET", // 18 alien + "Inv", // 19 invader + "Dagr", // 20 dagger + "Grim", // 21 grimace + "Fone", // 22 telephone + "Mtn", // 23 mountain + "End", // 24 end_arrow + "Ring", // 25 hollow_circle + "Drag", // 26 dragon + "Web", // 27 globe_meridians + "Eggp", // 28 eggplant + "Shld", // 29 shield + "Gogl", // 30 goggles + "Lzrd", // 31 lizard + "Zany", // 32 zany_face + "Roo", // 33 kangaroo + "Fthr", // 34 feather + "Sun", // 35 bright + "Wave", // 36 part_alt + "Boat", // 37 motorboat + "Card", // 38 playing_card + "Dish", // 39 satellite + "Pass", // 40 customs + "Cowb", // 41 cowboy + "Whl", // 42 wheel + "Koal", // 43 koala + "Knob", // 44 control_knobs + "Pch", // 45 peach + "Race", // 46 racing_car }; struct EmojiPicker { - int cursor; // 0 to EMOJI_COUNT-1 + int cursor; + int scrollRow; - EmojiPicker() : cursor(0) {} + EmojiPicker() : cursor(0), scrollRow(0) {} - void reset() { cursor = 0; } + void reset() { cursor = 0; scrollRow = 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 + void ensureVisible() { + int cursorRow = cursor / EMOJI_PICKER_COLS; + if (cursorRow < scrollRow) scrollRow = cursorRow; + else if (cursorRow >= scrollRow + EMOJI_PICKER_VISIBLE_ROWS) + scrollRow = cursorRow - EMOJI_PICKER_VISIBLE_ROWS + 1; + int maxScroll = EMOJI_PICKER_TOTAL_ROWS - EMOJI_PICKER_VISIBLE_ROWS; + if (maxScroll < 0) maxScroll = 0; + if (scrollRow > maxScroll) scrollRow = maxScroll; + if (scrollRow < 0) scrollRow = 0; + } + + // Returns emoji escape byte, 0xFF for cancel, 0 for no action uint8_t handleInput(char key) { - int col = cursor % EMOJI_PICKER_COLS; int row = cursor / EMOJI_PICKER_COLS; + int col = cursor % EMOJI_PICKER_COLS; switch (key) { - case 'w': case 'W': case 0xF2: // Up + case 'w': case 'W': case 0xF2: 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 + break; + case 's': case 'S': case 0xF1: + if (cursor + EMOJI_PICKER_COLS < EMOJI_COUNT) + cursor += EMOJI_PICKER_COLS; + else if (row < EMOJI_PICKER_TOTAL_ROWS - 1) + cursor = EMOJI_COUNT - 1; + break; + case 'a': case 'A': + if (cursor > 0) cursor--; + break; + case 'd': case 'D': if (cursor + 1 < EMOJI_COUNT) cursor++; - return 0; - case '\r': // Enter - select + break; + case '\r': + ensureVisible(); return (uint8_t)(EMOJI_ESCAPE_START + cursor); - case '\b': case 'q': case 'Q': case '$': // Cancel - return 0xFF; // Sentinel for "cancelled" + case '\b': case 'q': case 'Q': case '$': + return 0xFF; default: - return 0; // No action + return 0; } + ensureVisible(); + return 0; } void draw(DisplayDriver& display) { - // Header display.setTextSize(1); display.setCursor(0, 0); display.setColor(DisplayDriver::GREEN); @@ -81,55 +122,77 @@ struct EmojiPicker { display.setColor(DisplayDriver::LIGHT); display.drawRect(0, 11, display.width(), 1); - // Grid area - display.setTextSize(0); // Tiny font for labels + display.setTextSize(0); int startY = 14; - int cellW = display.width() / EMOJI_PICKER_COLS; - int cellH = (display.height() - startY - 14) / EMOJI_PICKER_ROWS; // Leave room for footer + int scrollBarW = 4; + int gridW = display.width() - scrollBarW - 1; + int cellW = gridW / EMOJI_PICKER_COLS; + int footerHeight = 14; + int gridH = display.height() - startY - footerHeight; + int cellH = gridH / EMOJI_PICKER_VISIBLE_ROWS; - 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; + for (int vr = 0; vr < EMOJI_PICKER_VISIBLE_ROWS; vr++) { + int absRow = scrollRow + vr; + if (absRow >= EMOJI_PICKER_TOTAL_ROWS) break; - // Draw selection border around highlighted cell - if (i == cursor) { + for (int col = 0; col < EMOJI_PICKER_COLS; col++) { + int idx = absRow * EMOJI_PICKER_COLS + col; + if (idx >= EMOJI_COUNT) break; + + int cx = col * cellW; + int cy = startY + vr * cellH; + + if (idx == cursor) { + display.setColor(DisplayDriver::LIGHT); + display.drawRect(cx, cy, cellW, cellH); + display.drawRect(cx + 1, cy + 1, cellW - 2, cellH - 2); + } + display.setColor(DisplayDriver::LIGHT); - display.drawRect(cx, cy, cellW, cellH); - display.drawRect(cx + 1, cy + 1, cellW - 2, cellH - 2); // Double border for visibility + const uint8_t* sprite = (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_LG[idx]); + 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); + } + + display.setColor(DisplayDriver::YELLOW); + uint16_t labelW = display.getTextWidth(EMOJI_LABELS[idx]); + int labelX = cx + (cellW - (int)labelW) / 2; + if (labelX < cx) labelX = cx; + display.setCursor(labelX, cy + 14); + display.print(EMOJI_LABELS[idx]); } - - // 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]); + } + + // Scroll bar + int sbX = display.width() - scrollBarW; + display.setColor(DisplayDriver::LIGHT); + display.drawRect(sbX, startY, scrollBarW, gridH); + + if (EMOJI_PICKER_TOTAL_ROWS > EMOJI_PICKER_VISIBLE_ROWS) { + int thumbH = (EMOJI_PICKER_VISIBLE_ROWS * gridH) / EMOJI_PICKER_TOTAL_ROWS; + if (thumbH < 4) thumbH = 4; + int maxScroll = EMOJI_PICKER_TOTAL_ROWS - EMOJI_PICKER_VISIBLE_ROWS; + int thumbY = startY + (scrollRow * (gridH - thumbH)) / maxScroll; + for (int y = thumbY + 1; y < thumbY + thumbH - 1; y++) + display.drawRect(sbX + 1, y, scrollBarW - 2, 1); + } else { + for (int y = startY + 1; y < startY + gridH - 1; y++) + display.drawRect(sbX + 1, y, scrollBarW - 2, 1); } // Footer display.setTextSize(1); int footerY = display.height() - 12; + display.setColor(DisplayDriver::LIGHT); 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); + const char* ct = "$:Back"; + display.setCursor(display.width() - display.getTextWidth(ct) - 2, footerY); + display.print(ct); } }; \ No newline at end of file