reordered emojis and improved scrolling in tdpro emojipicker; build emoji picker functionality into vkb for t5s3

This commit is contained in:
pelgraine
2026-04-12 22:01:17 +10:00
parent d2ce070a3f
commit 6d7fd54b83
3 changed files with 381 additions and 191 deletions
+104 -100
View File
@@ -3,7 +3,7 @@
// Emoji sprites for e-ink display - dual size
// Large (12x12) for compose/picker, Small (10x10) for channel view
// MSB-first, 2 bytes per row
// 65 total emoji: joy/thumbsup/frown first, then 43 original, then 19 new
// 76 total emoji: joy/thumbsup/frown first, then 43 original, then 19 new, then 11 newest
#include <stdint.h>
#ifdef ESP32
@@ -331,25 +331,29 @@ static const uint8_t emoji_lg_signal_strength[] PROGMEM = {
static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = {
emoji_lg_joy, emoji_lg_thumbsup, emoji_lg_frown,
// Faces/emotion first
emoji_lg_joy, emoji_lg_frown, emoji_lg_loudly_crying,
emoji_lg_grimace, emoji_lg_zany_face, emoji_lg_cowboy,
// Thumbsup + heart
emoji_lg_thumbsup, emoji_lg_heart,
// Everything else in original relative order
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_alien, emoji_lg_invader, emoji_lg_dagger,
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_eggplant, emoji_lg_shield, emoji_lg_goggles, emoji_lg_lizard,
emoji_lg_kangaroo, emoji_lg_feather, emoji_lg_bright, emoji_lg_part_alt, emoji_lg_motorboat,
emoji_lg_domino, emoji_lg_satellite, emoji_lg_customs, emoji_lg_cowboy, emoji_lg_wheel,
emoji_lg_domino, emoji_lg_satellite, emoji_lg_customs, emoji_lg_wheel,
emoji_lg_koala, emoji_lg_control_knobs, emoji_lg_peach, emoji_lg_racing_car,
emoji_lg_mouse, emoji_lg_mushroom, emoji_lg_biohazard, emoji_lg_panda,
emoji_lg_anger, emoji_lg_dragon_face, emoji_lg_pager, emoji_lg_bee,
emoji_lg_bulb, emoji_lg_cat, emoji_lg_fleur, emoji_lg_moon,
emoji_lg_coffee, emoji_lg_tooth, emoji_lg_pretzel, emoji_lg_abacus,
emoji_lg_moai, emoji_lg_tipping, emoji_lg_hedgehog,
emoji_lg_loudly_crying, emoji_lg_heart, emoji_lg_diamond_suit,
emoji_lg_spade_suit, emoji_lg_pizza, emoji_lg_four_leaf_clover,
emoji_lg_cloud, emoji_lg_rocket, emoji_lg_passport_control,
emoji_lg_eight_spoked_asterisk, emoji_lg_signal_strength,
emoji_lg_diamond_suit, emoji_lg_spade_suit, emoji_lg_pizza, emoji_lg_four_leaf_clover,
emoji_lg_cloud, emoji_lg_rocket, emoji_lg_passport_control,
emoji_lg_eight_spoked_asterisk, emoji_lg_signal_strength,
};
// ======== SMALL 10x10 SPRITES ========
@@ -614,114 +618,114 @@ static const uint8_t emoji_sm_signal_strength[] PROGMEM = {
};
static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = {
emoji_sm_joy, emoji_sm_thumbsup, emoji_sm_frown,
// Faces/emotion first
emoji_sm_joy, emoji_sm_frown, emoji_sm_loudly_crying,
emoji_sm_grimace, emoji_sm_zany_face, emoji_sm_cowboy,
// Thumbsup + heart
emoji_sm_thumbsup, emoji_sm_heart,
// Everything else in original relative order
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_alien, emoji_sm_invader, emoji_sm_dagger,
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_eggplant, emoji_sm_shield, emoji_sm_goggles, emoji_sm_lizard,
emoji_sm_kangaroo, emoji_sm_feather, emoji_sm_bright, emoji_sm_part_alt, emoji_sm_motorboat,
emoji_sm_domino, emoji_sm_satellite, emoji_sm_customs, emoji_sm_cowboy, emoji_sm_wheel,
emoji_sm_domino, emoji_sm_satellite, emoji_sm_customs, emoji_sm_wheel,
emoji_sm_koala, emoji_sm_control_knobs, emoji_sm_peach, emoji_sm_racing_car,
emoji_sm_mouse, emoji_sm_mushroom, emoji_sm_biohazard, emoji_sm_panda,
emoji_sm_anger, emoji_sm_dragon_face, emoji_sm_pager, emoji_sm_bee,
emoji_sm_bulb, emoji_sm_cat, emoji_sm_fleur, emoji_sm_moon,
emoji_sm_coffee, emoji_sm_tooth, emoji_sm_pretzel, emoji_sm_abacus,
emoji_sm_moai, emoji_sm_tipping, emoji_sm_hedgehog,
emoji_sm_loudly_crying,
emoji_sm_heart,
emoji_sm_diamond_suit,
emoji_sm_spade_suit,
emoji_sm_pizza,
emoji_sm_four_leaf_clover,
emoji_sm_cloud,
emoji_sm_rocket,
emoji_sm_passport_control,
emoji_sm_eight_spoked_asterisk,
emoji_sm_signal_strength,
emoji_sm_diamond_suit, emoji_sm_spade_suit, emoji_sm_pizza, emoji_sm_four_leaf_clover,
emoji_sm_cloud, emoji_sm_rocket, emoji_sm_passport_control,
emoji_sm_eight_spoked_asterisk, emoji_sm_signal_strength,
};
// ---- Codepoint lookup for UTF-8 conversion ----
struct EmojiCodepoint { uint32_t cp; uint32_t cp2; uint8_t escape; };
static const EmojiCodepoint EMOJI_CODEPOINTS[EMOJI_COUNT] = {
// Faces/emotion first
{ 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
{ 0x26F0, 0x0000, 0x96 }, // mountain
{ 0x1F51A, 0x0000, 0x97 }, // end_arrow
{ 0x2B55, 0x0000, 0x98 }, // hollow_circle
{ 0x1F409, 0x0000, 0x99 }, // dragon
{ 0x1F310, 0x0000, 0x9A }, // globe_meridians
{ 0x1F346, 0x0000, 0x9B }, // eggplant
{ 0x1F6E1, 0x0000, 0x9C }, // shield
{ 0x1F97D, 0x0000, 0x9D }, // goggles
{ 0x1F98E, 0x0000, 0x9E }, // lizard
{ 0x1F92A, 0x0000, 0x9F }, // zany_face
{ 0x1F998, 0x0000, 0xA0 }, // kangaroo
{ 0x1FAB6, 0x0000, 0xA1 }, // feather
{ 0x1F506, 0x0000, 0xA2 }, // bright
{ 0x303D, 0x0000, 0xA3 }, // part_alt
{ 0x1F6E5, 0x0000, 0xA4 }, // motorboat
{ 0x1F030, 0x0000, 0xA5 }, // domino
{ 0x1F4E1, 0x0000, 0xA6 }, // satellite
{ 0x1F6C3, 0x0000, 0xA7 }, // customs
{ 0x1F920, 0x0000, 0xA8 }, // cowboy
{ 0x1F6DE, 0x0000, 0xA9 }, // wheel
{ 0x1F428, 0x0000, 0xAA }, // koala
{ 0x1F39B, 0x0000, 0xAB }, // control_knobs
{ 0x1F351, 0x0000, 0xAC }, // peach
{ 0x1F3CE, 0x0000, 0xAD }, // racing_car
{ 0x1F42D, 0x0000, 0xAE }, // mouse
{ 0x1F344, 0x0000, 0xAF }, // mushroom
{ 0x2623, 0x0000, 0xB0 }, // biohazard
{ 0x1F43C, 0x0000, 0xB1 }, // panda
{ 0x1F4A2, 0x0000, 0xB2 }, // anger
{ 0x1F432, 0x0000, 0xB3 }, // dragon_face
{ 0x1F4DF, 0x0000, 0xB4 }, // pager
{ 0x1F41D, 0x0000, 0xB5 }, // bee
{ 0x1F4A1, 0x0000, 0xB6 }, // bulb
{ 0x1F431, 0x0000, 0xB7 }, // cat
{ 0x269C, 0x0000, 0xB8 }, // fleur
{ 0x1F314, 0x0000, 0xB9 }, // moon
{ 0x2615, 0x0000, 0xBA }, // coffee
{ 0x1F9B7, 0x0000, 0xBB }, // tooth
{ 0x1F968, 0x0000, 0xBC }, // pretzel
{ 0x1F9EE, 0x0000, 0xBD }, // abacus
{ 0x1F5FF, 0x0000, 0xBE }, // moai
{ 0x1F481, 0x0000, 0xBF }, // tipping
{ 0x1F994, 0x0000, 0xC0 }, // hedgehog
{ 0x1F62D , 0x0000 , 0xC1 }, // loudly_crying
{ 0x2665 , 0x0000 , 0xC2 }, // heart
{ 0x2666 , 0x0000 , 0xC3 }, // diamond_suit
{ 0x2660 , 0x0000 , 0xC4 }, // spade_suit
{ 0x1F355 , 0x0000 , 0xC5 }, // pizza
{ 0x1F340 , 0x0000 , 0xC6 }, // four_leaf_clover
{ 0x2601 , 0x0000 , 0xC7 }, // cloud
{ 0x1F680 , 0x0000 , 0xC8 }, // rocket
{ 0x1F6C2 , 0x0000 , 0xC9 }, // passport_control
{ 0x2733 , 0x0000 , 0xCA }, // eight_spoked_asterisk
{ 0x1F4F6 , 0x0000 , 0xCB }, // signal_strength
{ 0x2639, 0x0000, 0x81 }, // frown
{ 0x1F62D, 0x0000, 0x82 }, // loudly_crying
{ 0x1F62C, 0x0000, 0x83 }, // grimace
{ 0x1F92A, 0x0000, 0x84 }, // zany_face
{ 0x1F920, 0x0000, 0x85 }, // cowboy
// Thumbsup + heart
{ 0x1F44D, 0x0000, 0x86 }, // thumbsup
{ 0x2665, 0x0000, 0x87 }, // heart
// Everything else in original relative order
{ 0x1F6DC, 0x0000, 0x88 }, // wireless
{ 0x267E, 0x0000, 0x89 }, // infinity
{ 0x1F996, 0x0000, 0x8A }, // trex
{ 0x2620, 0x0000, 0x8B }, // skull
{ 0x271D, 0x0000, 0x8C }, // cross
{ 0x26A1, 0x0000, 0x8D }, // lightning
{ 0x1F3A9, 0x0000, 0x8E }, // tophat
{ 0x1F3CD, 0x0000, 0x8F }, // motorcycle
{ 0x1F331, 0x0000, 0x90 }, // seedling
{ 0x1F1E6, 0x1F1FA, 0x91 }, // flag_au
{ 0x2602, 0x0000, 0x92 }, // umbrella
{ 0x1F9FF, 0x0000, 0x93 }, // nazar
{ 0x1F30F, 0x0000, 0x94 }, // globe
{ 0x2622, 0x0000, 0x95 }, // radioactive
{ 0x1F404, 0x0000, 0x96 }, // cow
{ 0x1F47D, 0x0000, 0x97 }, // alien
{ 0x1F47E, 0x0000, 0x98 }, // invader
{ 0x1F5E1, 0x0000, 0x99 }, // dagger
{ 0x26F0, 0x0000, 0x9A }, // mountain
{ 0x1F51A, 0x0000, 0x9B }, // end_arrow
{ 0x2B55, 0x0000, 0x9C }, // hollow_circle
{ 0x1F409, 0x0000, 0x9D }, // dragon
{ 0x1F310, 0x0000, 0x9E }, // globe_meridians
{ 0x1F346, 0x0000, 0x9F }, // eggplant
{ 0x1F6E1, 0x0000, 0xA0 }, // shield
{ 0x1F97D, 0x0000, 0xA1 }, // goggles
{ 0x1F98E, 0x0000, 0xA2 }, // lizard
{ 0x1F998, 0x0000, 0xA3 }, // kangaroo
{ 0x1FAB6, 0x0000, 0xA4 }, // feather
{ 0x1F506, 0x0000, 0xA5 }, // bright
{ 0x303D, 0x0000, 0xA6 }, // part_alt
{ 0x1F6E5, 0x0000, 0xA7 }, // motorboat
{ 0x1F030, 0x0000, 0xA8 }, // domino
{ 0x1F4E1, 0x0000, 0xA9 }, // satellite
{ 0x1F6C3, 0x0000, 0xAA }, // customs
{ 0x1F6DE, 0x0000, 0xAB }, // wheel
{ 0x1F428, 0x0000, 0xAC }, // koala
{ 0x1F39B, 0x0000, 0xAD }, // control_knobs
{ 0x1F351, 0x0000, 0xAE }, // peach
{ 0x1F3CE, 0x0000, 0xAF }, // racing_car
{ 0x1F42D, 0x0000, 0xB0 }, // mouse
{ 0x1F344, 0x0000, 0xB1 }, // mushroom
{ 0x2623, 0x0000, 0xB2 }, // biohazard
{ 0x1F43C, 0x0000, 0xB3 }, // panda
{ 0x1F4A2, 0x0000, 0xB4 }, // anger
{ 0x1F432, 0x0000, 0xB5 }, // dragon_face
{ 0x1F4DF, 0x0000, 0xB6 }, // pager
{ 0x1F41D, 0x0000, 0xB7 }, // bee
{ 0x1F4A1, 0x0000, 0xB8 }, // bulb
{ 0x1F431, 0x0000, 0xB9 }, // cat
{ 0x269C, 0x0000, 0xBA }, // fleur
{ 0x1F314, 0x0000, 0xBB }, // moon
{ 0x2615, 0x0000, 0xBC }, // coffee
{ 0x1F9B7, 0x0000, 0xBD }, // tooth
{ 0x1F968, 0x0000, 0xBE }, // pretzel
{ 0x1F9EE, 0x0000, 0xBF }, // abacus
{ 0x1F5FF, 0x0000, 0xC0 }, // moai
{ 0x1F481, 0x0000, 0xC1 }, // tipping
{ 0x1F994, 0x0000, 0xC2 }, // hedgehog
{ 0x2666, 0x0000, 0xC3 }, // diamond_suit
{ 0x2660, 0x0000, 0xC4 }, // spade_suit
{ 0x1F355, 0x0000, 0xC5 }, // pizza
{ 0x1F340, 0x0000, 0xC6 }, // four_leaf_clover
{ 0x2601, 0x0000, 0xC7 }, // cloud
{ 0x1F680, 0x0000, 0xC8 }, // rocket
{ 0x1F6C2, 0x0000, 0xC9 }, // passport_control
{ 0x2733, 0x0000, 0xCA }, // eight_spoked_asterisk
{ 0x1F4F6, 0x0000, 0xCB }, // signal_strength
};
// ---- Helper functions ----
@@ -731,7 +735,7 @@ static const EmojiCodepoint EMOJI_CODEPOINTS[EMOJI_COUNT] = {
struct EmojiAlias { uint32_t cp; uint8_t escape; };
#define EMOJI_ALIAS_COUNT 1
static const EmojiAlias EMOJI_ALIASES[EMOJI_ALIAS_COUNT] = {
{ 0x1F08E, 0xA5 }, // domino tile (MWD node signifier) -> domino sprite
{ 0x1F08E, 0xA8 }, // domino tile (MWD node signifier) -> domino sprite
};
static uint32_t emojiDecodeUtf8(const uint8_t* s, int remaining, int* bytes_consumed) {
+83 -70
View File
@@ -12,80 +12,83 @@
#define EMOJI_PICKER_TOTAL_ROWS ((EMOJI_COUNT + EMOJI_PICKER_COLS - 1) / EMOJI_PICKER_COLS)
static const char* EMOJI_LABELS[EMOJI_COUNT] = {
// Faces/emotion
"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
"Mtn", // 22 mountain
"End", // 23 end_arrow
"Ring", // 24 hollow_circle
"Drag", // 25 dragon
"Web", // 26 globe_meridians
"Eggp", // 27 eggplant
"Shld", // 28 shield
"Gogl", // 29 goggles
"Lzrd", // 30 lizard
"Zany", // 31 zany_face
"Roo", // 32 kangaroo
"Fthr", // 33 feather
"Sun", // 34 bright
"Wave", // 35 part_alt
"Boat", // 36 motorboat
"Domi", // 37 domino
"Dish", // 38 satellite
"Pass", // 39 customs
"Cowb", // 40 cowboy
"Whl", // 41 wheel
"Koal", // 42 koala
"Knob", // 43 control_knobs
"Pch", // 44 peach
"Race", // 45 racing_car
"Mous", // 46 mouse
"Shrm", // 47 mushroom
"Bio", // 48 biohazard
"Pnda", // 49 panda
"Bang", // 50 anger
"DrgF", // 51 dragon_face
"Pagr", // 52 pager
"Bee", // 53 bee
"Bulb", // 54 bulb
"Cat", // 55 cat
"Flur", // 56 fleur
"Moon", // 57 moon
"Cafe", // 58 coffee
"Toth", // 59 tooth
"Prtz", // 60 pretzel
"Abac", // 61 abacus
"Moai", // 62 moai
"Hiii", // 63 tipping
"Hedg", // 64 hedgehog
"Cry", // 65 loudly_crying
"Love", // 66 heart
"Sad", // 1 frown
"Cry", // 2 loudly_crying
"Grim", // 3 grimace
"Zany", // 4 zany_face
"Cowb", // 5 cowboy
// Thumbsup + heart
"Like", // 6 thumbsup
"Love", // 7 heart
// Everything else
"WiFi", // 8 wireless
"Inf", // 9 infinity
"Rex", // 10 trex
"Skul", // 11 skull
"Cros", // 12 cross
"Bolt", // 13 lightning
"Hat", // 14 tophat
"Moto", // 15 motorcycle
"Leaf", // 16 seedling
"AU", // 17 flag_au
"Umbr", // 18 umbrella
"Eye", // 19 nazar
"Glob", // 20 globe
"Rad", // 21 radioactive
"Cow", // 22 cow
"ET", // 23 alien
"Inv", // 24 invader
"Dagr", // 25 dagger
"Mtn", // 26 mountain
"End", // 27 end_arrow
"Ring", // 28 hollow_circle
"Drag", // 29 dragon
"Web", // 30 globe_meridians
"Eggp", // 31 eggplant
"Shld", // 32 shield
"Gogl", // 33 goggles
"Lzrd", // 34 lizard
"Roo", // 35 kangaroo
"Fthr", // 36 feather
"Sun", // 37 bright
"Wave", // 38 part_alt
"Boat", // 39 motorboat
"Domi", // 40 domino
"Dish", // 41 satellite
"Pass", // 42 customs
"Whl", // 43 wheel
"Koal", // 44 koala
"Knob", // 45 control_knobs
"Pch", // 46 peach
"Race", // 47 racing_car
"Mous", // 48 mouse
"Shrm", // 49 mushroom
"Bio", // 50 biohazard
"Pnda", // 51 panda
"Bang", // 52 anger
"DrgF", // 53 dragon_face
"Pagr", // 54 pager
"Bee", // 55 bee
"Bulb", // 56 bulb
"Cat", // 57 cat
"Flur", // 58 fleur
"Moon", // 59 moon
"Cafe", // 60 coffee
"Toth", // 61 tooth
"Prtz", // 62 pretzel
"Abac", // 63 abacus
"Moai", // 64 moai
"Hiii", // 65 tipping
"Hedg", // 66 hedgehog
"Diam", // 67 diamond_suit
"Spde", // 68 spade_suit
"Piza", // 69 pizza
"Luck", // 70 four_leaf_clover
"Cld", // 71 cloud
"Rckt", // 72 rocket
"Visa", // 73 passport_control
"HFC", // 73 passport_control
"Star", // 74 eight_spoked_asterisk
"Sig", // 75 signal_strength
};
@@ -116,13 +119,23 @@ struct EmojiPicker {
switch (key) {
case 'w': case 'W': case 0xF2:
if (row > 0) cursor -= EMOJI_PICKER_COLS;
if (row > 0) {
cursor -= EMOJI_PICKER_COLS;
} else {
// Wrap to last row, same column
int target = (EMOJI_PICKER_TOTAL_ROWS - 1) * EMOJI_PICKER_COLS + col;
cursor = (target >= EMOJI_COUNT) ? EMOJI_COUNT - 1 : target;
}
break;
case 's': case 'S': case 0xF1:
if (cursor + EMOJI_PICKER_COLS < EMOJI_COUNT)
if (cursor + EMOJI_PICKER_COLS < EMOJI_COUNT) {
cursor += EMOJI_PICKER_COLS;
else if (row < EMOJI_PICKER_TOTAL_ROWS - 1)
} else if (row < EMOJI_PICKER_TOTAL_ROWS - 1) {
cursor = EMOJI_COUNT - 1;
} else {
// Wrap to first row, same column
cursor = col;
}
break;
case 'a': case 'A':
if (cursor > 0) cursor--;
+194 -21
View File
@@ -18,6 +18,7 @@
#include <Arduino.h>
#include <helpers/ui/DisplayDriver.h>
#include "EmojiSprites.h"
enum VKBStatus { VKB_EDITING, VKB_SUBMITTED, VKB_CANCELLED };
@@ -45,7 +46,8 @@ public:
static const int MAX_TEXT = 140;
VirtualKeyboard() : _status(VKB_CANCELLED), _purpose(VKB_CHANNEL_MSG),
_contextIdx(0), _textLen(0), _shifted(false), _symbols(false) {
_contextIdx(0), _textLen(0), _shifted(false), _symbols(false),
_emojiMode(false), _emojiScroll(0) {
_text[0] = '\0';
_label[0] = '\0';
}
@@ -56,6 +58,8 @@ public:
_status = VKB_EDITING;
_shifted = false;
_symbols = false;
_emojiMode = false;
_emojiScroll = 0;
_maxLen = (maxLen > 0 && maxLen < MAX_TEXT) ? maxLen : MAX_TEXT;
strncpy(_label, label, sizeof(_label) - 1);
@@ -90,13 +94,8 @@ public:
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, 10, 128, 18); // Border
display.setCursor(2, 12);
display.setColor(DisplayDriver::LIGHT);
// Show text with cursor
char dispBuf[MAX_TEXT + 2];
snprintf(dispBuf, sizeof(dispBuf), "%s_", _text);
display.print(dispBuf);
// Render text with inline emoji sprites
renderTextField(display);
// Character count
{
@@ -111,6 +110,11 @@ public:
// Separator
display.drawRect(0, 30, 128, 1);
if (_emojiMode) {
renderEmojiGrid(display);
return;
}
// --- Draw keyboard rows ---
const char* const* layout = getLayout();
@@ -183,6 +187,8 @@ public:
bool handleTap(int vx, int vy) {
if (_status != VKB_EDITING) return false;
if (_emojiMode) return handleEmojiTap(vx, vy);
// Check keyboard rows 0-2
const char* const* layout = getLayout();
@@ -253,6 +259,169 @@ private:
char _label[40];
bool _shifted;
bool _symbols;
bool _emojiMode;
int _emojiScroll;
// Emoji grid constants (virtual coords)
static const int EMJ_COLS = 8;
static const int EMJ_CELL = 15; // 12px sprite + 3px gap
static const int EMJ_GRID_X = 4;
static const int EMJ_GRID_Y = 34;
static const int EMJ_VIS_ROWS = 5;
int emojiTotalRows() const { return (EMOJI_COUNT + EMJ_COLS - 1) / EMJ_COLS; }
int emojiMaxScroll() const { int m = emojiTotalRows() - EMJ_VIS_ROWS; return m < 0 ? 0 : m; }
void renderEmojiGrid(DisplayDriver& display) {
display.setTextSize(0);
for (int vr = 0; vr < EMJ_VIS_ROWS; vr++) {
int absRow = _emojiScroll + vr;
if (absRow >= emojiTotalRows()) break;
for (int col = 0; col < EMJ_COLS; col++) {
int idx = absRow * EMJ_COLS + col;
if (idx >= EMOJI_COUNT) break;
int cx = EMJ_GRID_X + col * EMJ_CELL;
int cy = EMJ_GRID_Y + vr * EMJ_CELL;
display.setColor(DisplayDriver::LIGHT);
const uint8_t* sprite = (const uint8_t*)pgm_read_ptr(&EMOJI_SPRITES_LG[idx]);
if (sprite) {
display.drawXbm(cx + 1, cy + 1, sprite, EMOJI_LG_W, EMOJI_LG_H);
}
}
}
// Footer: [Back] [▲] page/total [▼]
int fy = EMJ_GRID_Y + EMJ_VIS_ROWS * EMJ_CELL + 2;
display.setColor(DisplayDriver::LIGHT);
display.drawRect(0, fy - 1, 128, 1);
// Back button (inverted)
display.fillRect(4, fy + 1, 30, 12);
display.setColor(DisplayDriver::DARK);
int bw = display.getTextWidth("Back");
display.setCursor(4 + (30 - bw) / 2, fy + 2);
display.print("Back");
display.setColor(DisplayDriver::LIGHT);
// Scroll arrows (only if scrollable)
if (emojiTotalRows() > EMJ_VIS_ROWS) {
// Up arrow
if (_emojiScroll > 0) {
display.fillRect(50, fy + 1, 12, 12);
display.setColor(DisplayDriver::DARK);
display.setCursor(53, fy + 2);
display.print("^");
display.setColor(DisplayDriver::LIGHT);
}
// Page info
char pg[8];
snprintf(pg, sizeof(pg), "%d/%d", _emojiScroll + 1, emojiMaxScroll() + 1);
int pw = display.getTextWidth(pg);
display.setCursor(75 - pw / 2, fy + 2);
display.print(pg);
// Down arrow
if (_emojiScroll < emojiMaxScroll()) {
display.fillRect(90, fy + 1, 12, 12);
display.setColor(DisplayDriver::DARK);
display.setCursor(93, fy + 2);
display.print("v");
display.setColor(DisplayDriver::LIGHT);
}
}
}
bool handleEmojiTap(int vx, int vy) {
int fy = EMJ_GRID_Y + EMJ_VIS_ROWS * EMJ_CELL + 2;
// Footer area
if (vy >= fy) {
if (vx >= 4 && vx < 34) {
// Back button
_emojiMode = false;
return true;
}
if (vx >= 50 && vx < 62 && _emojiScroll > 0) {
_emojiScroll--;
return true;
}
if (vx >= 90 && vx < 102 && _emojiScroll < emojiMaxScroll()) {
_emojiScroll++;
return true;
}
return true; // Consume tap in footer
}
// Grid area
if (vy >= EMJ_GRID_Y && vy < EMJ_GRID_Y + EMJ_VIS_ROWS * EMJ_CELL) {
int col = (vx - EMJ_GRID_X) / EMJ_CELL;
int vr = (vy - EMJ_GRID_Y) / EMJ_CELL;
if (col < 0 || col >= EMJ_COLS || vr < 0 || vr >= EMJ_VIS_ROWS) return true;
int idx = (_emojiScroll + vr) * EMJ_COLS + col;
if (idx >= 0 && idx < EMOJI_COUNT) {
insertEmoji(idx);
_emojiMode = false;
}
return true;
}
return true; // Consume any tap while in emoji mode
}
void insertEmoji(int idx) {
// Insert as UTF-8 directly (not escape bytes) so sent messages are valid
uint8_t utf8[8];
int len = emojiEncodeUtf8(EMOJI_CODEPOINTS[idx].cp, utf8);
if (EMOJI_CODEPOINTS[idx].cp2 != 0)
len += emojiEncodeUtf8(EMOJI_CODEPOINTS[idx].cp2, utf8 + len);
if (_textLen + len > _maxLen) return;
memcpy(_text + _textLen, utf8, len);
_textLen += len;
_text[_textLen] = '\0';
}
// Render text field with inline emoji sprites (10×10)
void renderTextField(DisplayDriver& display) {
// Convert UTF-8 emoji to escape bytes for sprite lookup
char sanitized[MAX_TEXT + 1];
emojiSanitize(_text, sanitized, sizeof(sanitized));
int x = 2;
int maxX = 124;
display.setColor(DisplayDriver::LIGHT);
display.setTextSize(0);
for (int i = 0; sanitized[i] && x < maxX; i++) {
uint8_t b = (uint8_t)sanitized[i];
if (b == EMOJI_PAD_BYTE) continue;
if (isEmojiEscape(b)) {
const uint8_t* sprite = getEmojiSpriteSm(b);
if (sprite && x + EMOJI_SM_W < maxX) {
display.drawXbm(x, 14, sprite, EMOJI_SM_W, EMOJI_SM_H);
x += EMOJI_SM_W + 1;
}
} else {
char ch[2] = { (char)b, '\0' };
display.setCursor(x, 12);
display.print(ch);
x += display.getTextWidth(ch);
}
}
// Blinking cursor
if (x < maxX) {
display.setCursor(x, 12);
display.print("_");
}
}
// Layout constants (virtual coords)
static const int KEY_W = 11;
@@ -261,7 +430,7 @@ private:
static const int KEY_START_Y = 34;
// Key layouts — rows 0-2 as char arrays
// Special: ^ = shift, < = backspace, # = symbols, > = enter, ~ = space
// Special: ^ = shift, < = backspace, \x01 = sym toggle, \x02 = emoji, > = enter, ~ = space
const char* const* getLayout() const {
static const char* const lower[3] = { "qwertyuiop", "asdfghjkl", "^zxcvbnm<" };
static const char* const upper[3] = { "QWERTYUIOP", "ASDFGHJKL", "^ZXCVBNM<" };
@@ -269,26 +438,22 @@ private:
return _symbols ? syms : (_shifted ? upper : lower);
}
// Row 4: variable-width keys [#/ABC] [,] [SPACE] [.] [Enter]
// Row 4: variable-width keys [#/ABC] [,] [$] [SPACE] [.] [Enter]
// Defined by physical zones, not the char-array approach
struct R4Key { int x; int w; char ch; const char* label; };
void drawRow4(DisplayDriver& display, int y) {
// # or ABC toggle: x=4, w=20
// comma: x=26, w=11
// space: x=39, w=50
// period: x=91, w=11
// enter: x=104, w=20
const R4Key keys[] = {
{ 4, 20, '\x01', _symbols ? "ABC" : "123" },
{ 26, 11, ',', "," },
{ 39, 50, '~', "space" },
{ 39, 11, '\x02', "$" },
{ 52, 37, '~', "space" },
{ 91, 11, '.', "." },
{ 104, 20, '>', "Send" }
};
for (int i = 0; i < 5; i++) {
bool special = (keys[i].ch == '\x01' || keys[i].ch == '>');
for (int i = 0; i < 6; i++) {
bool special = (keys[i].ch == '\x01' || keys[i].ch == '>' || keys[i].ch == '\x02');
if (special) {
display.setColor(DisplayDriver::LIGHT);
display.fillRect(keys[i].x, y + 1, keys[i].w, KEY_H - 1);
@@ -312,11 +477,12 @@ private:
const R4Key keys[] = {
{ 4, 20, '\x01', nullptr },
{ 26, 11, ',', nullptr },
{ 39, 50, '~', nullptr },
{ 39, 11, '\x02', nullptr },
{ 52, 37, '~', nullptr },
{ 91, 11, '.', nullptr },
{ 104, 20, '>', nullptr }
};
for (int i = 0; i < 5; i++) {
for (int i = 0; i < 6; i++) {
if (vx >= keys[i].x && vx < keys[i].x + keys[i].w) {
processKey(keys[i].ch);
return true;
@@ -334,10 +500,17 @@ private:
// Symbol/letter toggle
_symbols = !_symbols;
_shifted = false;
} else if (ch == '\x02') {
// Emoji picker toggle
_emojiMode = !_emojiMode;
_emojiScroll = 0;
} else if (ch == '<') {
// Backspace
// Backspace — UTF-8 aware (walk back past continuation bytes 10xxxxxx)
if (_textLen > 0) {
_textLen--;
while (_textLen > 0 && ((uint8_t)_text[_textLen] & 0xC0) == 0x80) {
_textLen--;
}
_text[_textLen] = '\0';
}
} else if (ch == '>') {