After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 175 B |
After Width: | Height: | Size: 164 B |
After Width: | Height: | Size: 125 B |
After Width: | Height: | Size: 162 B |
After Width: | Height: | Size: 342 B |
After Width: | Height: | Size: 154 B |
After Width: | Height: | Size: 157 B |
After Width: | Height: | Size: 170 B |
After Width: | Height: | Size: 159 B |
After Width: | Height: | Size: 165 B |
After Width: | Height: | Size: 156 B |
After Width: | Height: | Size: 166 B |
After Width: | Height: | Size: 176 B |
After Width: | Height: | Size: 178 B |
After Width: | Height: | Size: 178 B |
After Width: | Height: | Size: 142 B |
@ -0,0 +1,203 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// - the first two bytes are width and height of the glyph
|
||||
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
|
||||
// - the rest is packed 1-bit glyph data
|
||||
|
||||
/* */ static const uint8_t Font_Unifont_Bold_16_glyph_32[] = { 0, 0, 7, 0, 0 }; // hand-changed to 7 to have 9px space between words
|
||||
/* ! */ static const uint8_t Font_Unifont_Bold_16_glyph_33[] = { 2, 10, 7, 2, 10, 255, 252, 240 };
|
||||
/* " */ static const uint8_t Font_Unifont_Bold_16_glyph_34[] = { 6, 4, 7, 0, 12, 207, 60, 209, 0 };
|
||||
/* # */ static const uint8_t Font_Unifont_Bold_16_glyph_35[] = { 7, 10, 8, 0, 10, 54, 108, 223, 246, 205, 191, 236, 217, 176 };
|
||||
/* $ */ static const uint8_t Font_Unifont_Bold_16_glyph_36[] = { 7, 10, 8, 0, 10, 16, 251, 94, 183, 3, 181, 235, 124, 32 };
|
||||
/* % */ static const uint8_t Font_Unifont_Bold_16_glyph_37[] = { 7, 10, 8, 0, 10, 97, 102, 211, 65, 2, 11, 43, 150, 24 };
|
||||
/* & */ static const uint8_t Font_Unifont_Bold_16_glyph_38[] = { 8, 10, 8, 0, 10, 56, 108, 108, 104, 48, 107, 206, 204, 206, 122, 0 };
|
||||
/* ' */ static const uint8_t Font_Unifont_Bold_16_glyph_39[] = { 2, 4, 7, 2, 12, 253, 0 };
|
||||
/* ( */ static const uint8_t Font_Unifont_Bold_16_glyph_40[] = { 4, 12, 7, 2, 11, 54, 108, 204, 204, 198, 99, 0 };
|
||||
/* ) */ static const uint8_t Font_Unifont_Bold_16_glyph_41[] = { 4, 12, 7, 0, 11, 198, 99, 51, 51, 54, 108, 0 };
|
||||
/* * */ static const uint8_t Font_Unifont_Bold_16_glyph_42[] = { 7, 7, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 };
|
||||
/* + */ static const uint8_t Font_Unifont_Bold_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 };
|
||||
/* , */ static const uint8_t Font_Unifont_Bold_16_glyph_44[] = { 3, 4, 7, 1, 2, 237, 224 };
|
||||
/* - */ static const uint8_t Font_Unifont_Bold_16_glyph_45[] = { 5, 1, 7, 0, 5, 248 };
|
||||
/* . */ static const uint8_t Font_Unifont_Bold_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 };
|
||||
/* / */ static const uint8_t Font_Unifont_Bold_16_glyph_47[] = { 6, 10, 7, 0, 10, 12, 49, 132, 48, 194, 24, 195, 0 };
|
||||
/* 0 */ static const uint8_t Font_Unifont_Bold_16_glyph_48[] = { 7, 10, 8, 0, 10, 56, 219, 30, 125, 122, 249, 227, 108, 112 };
|
||||
/* 1 */ static const uint8_t Font_Unifont_Bold_16_glyph_49[] = { 6, 10, 7, 0, 10, 51, 195, 12, 48, 195, 12, 51, 240 };
|
||||
/* 2 */ static const uint8_t Font_Unifont_Bold_16_glyph_50[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 12, 112, 193, 252 };
|
||||
/* 3 */ static const uint8_t Font_Unifont_Bold_16_glyph_51[] = { 7, 10, 8, 0, 10, 125, 143, 24, 49, 192, 193, 227, 198, 248 };
|
||||
/* 4 */ static const uint8_t Font_Unifont_Bold_16_glyph_52[] = { 7, 10, 8, 0, 10, 12, 120, 179, 100, 217, 191, 134, 12, 24 };
|
||||
/* 5 */ static const uint8_t Font_Unifont_Bold_16_glyph_53[] = { 7, 10, 8, 0, 10, 255, 131, 6, 15, 193, 193, 227, 198, 248 };
|
||||
/* 6 */ static const uint8_t Font_Unifont_Bold_16_glyph_54[] = { 7, 10, 8, 0, 10, 60, 195, 6, 15, 216, 241, 227, 198, 248 };
|
||||
/* 7 */ static const uint8_t Font_Unifont_Bold_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 48, 195, 24, 99, 12, 97, 128 };
|
||||
/* 8 */ static const uint8_t Font_Unifont_Bold_16_glyph_56[] = { 7, 10, 8, 0, 10, 125, 143, 30, 55, 216, 241, 227, 198, 248 };
|
||||
/* 9 */ static const uint8_t Font_Unifont_Bold_16_glyph_57[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 111, 193, 131, 12, 240 };
|
||||
/* : */ static const uint8_t Font_Unifont_Bold_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 };
|
||||
/* ; */ static const uint8_t Font_Unifont_Bold_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 61, 128 };
|
||||
/* < */ static const uint8_t Font_Unifont_Bold_16_glyph_60[] = { 6, 9, 7, 0, 9, 12, 99, 24, 193, 131, 6, 12 };
|
||||
/* = */ static const uint8_t Font_Unifont_Bold_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 };
|
||||
/* > */ static const uint8_t Font_Unifont_Bold_16_glyph_62[] = { 6, 9, 7, 0, 9, 193, 131, 6, 12, 99, 24, 192 };
|
||||
/* ? */ static const uint8_t Font_Unifont_Bold_16_glyph_63[] = { 7, 10, 8, 0, 10, 125, 143, 24, 48, 195, 6, 0, 24, 48 };
|
||||
/* @ */ static const uint8_t Font_Unifont_Bold_16_glyph_64[] = { 7, 10, 8, 0, 10, 60, 134, 109, 187, 118, 237, 205, 64, 124 };
|
||||
/* A */ static const uint8_t Font_Unifont_Bold_16_glyph_65[] = { 7, 10, 8, 0, 10, 56, 249, 182, 60, 120, 255, 227, 199, 140 };
|
||||
/* B */ static const uint8_t Font_Unifont_Bold_16_glyph_66[] = { 7, 10, 8, 0, 10, 253, 143, 30, 63, 216, 241, 227, 199, 248 };
|
||||
/* C */ static const uint8_t Font_Unifont_Bold_16_glyph_67[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 24, 48, 99, 198, 248 };
|
||||
/* D */ static const uint8_t Font_Unifont_Bold_16_glyph_68[] = { 7, 10, 8, 0, 10, 241, 155, 30, 60, 120, 241, 227, 205, 224 };
|
||||
/* E */ static const uint8_t Font_Unifont_Bold_16_glyph_69[] = { 6, 10, 7, 0, 10, 255, 12, 48, 251, 12, 48, 195, 240 };
|
||||
/* F */ static const uint8_t Font_Unifont_Bold_16_glyph_70[] = { 6, 10, 7, 0, 10, 255, 12, 48, 195, 236, 48, 195, 0 };
|
||||
/* G */ static const uint8_t Font_Unifont_Bold_16_glyph_71[] = { 7, 10, 8, 0, 10, 125, 143, 30, 12, 27, 241, 227, 206, 236 };
|
||||
/* H */ static const uint8_t Font_Unifont_Bold_16_glyph_72[] = { 7, 10, 8, 0, 10, 199, 143, 30, 63, 248, 241, 227, 199, 140 };
|
||||
/* I */ static const uint8_t Font_Unifont_Bold_16_glyph_73[] = { 6, 10, 7, 0, 10, 252, 195, 12, 48, 195, 12, 51, 240 };
|
||||
/* J */ static const uint8_t Font_Unifont_Bold_16_glyph_74[] = { 7, 10, 8, 0, 10, 62, 24, 48, 96, 193, 131, 102, 204, 240 };
|
||||
/* K */ static const uint8_t Font_Unifont_Bold_16_glyph_75[] = { 7, 10, 8, 0, 10, 199, 143, 54, 207, 28, 62, 110, 207, 140 };
|
||||
/* L */ static const uint8_t Font_Unifont_Bold_16_glyph_76[] = { 6, 10, 7, 0, 10, 195, 12, 48, 195, 12, 48, 195, 240 };
|
||||
/* M */ static const uint8_t Font_Unifont_Bold_16_glyph_77[] = { 7, 10, 8, 0, 10, 131, 143, 31, 127, 250, 245, 227, 199, 140 };
|
||||
/* N */ static const uint8_t Font_Unifont_Bold_16_glyph_78[] = { 7, 10, 8, 0, 10, 199, 207, 158, 189, 122, 245, 231, 207, 140 };
|
||||
/* O */ static const uint8_t Font_Unifont_Bold_16_glyph_79[] = { 7, 10, 8, 0, 10, 125, 143, 30, 60, 120, 241, 227, 198, 248 };
|
||||
/* P */ static const uint8_t Font_Unifont_Bold_16_glyph_80[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 176, 96, 193, 128 };
|
||||
/* Q */ static const uint8_t Font_Unifont_Bold_16_glyph_81[] = { 7, 11, 8, 0, 10, 125, 143, 30, 60, 120, 241, 235, 238, 112, 24 };
|
||||
/* R */ static const uint8_t Font_Unifont_Bold_16_glyph_82[] = { 7, 10, 8, 0, 10, 253, 143, 30, 60, 127, 182, 102, 199, 140 };
|
||||
/* S */ static const uint8_t Font_Unifont_Bold_16_glyph_83[] = { 7, 10, 8, 0, 10, 125, 143, 31, 7, 135, 131, 227, 198, 248 };
|
||||
/* T */ static const uint8_t Font_Unifont_Bold_16_glyph_84[] = { 7, 10, 8, 0, 10, 254, 48, 96, 193, 131, 6, 12, 24, 48 };
|
||||
/* U */ static const uint8_t Font_Unifont_Bold_16_glyph_85[] = { 7, 10, 8, 0, 10, 199, 143, 30, 60, 120, 241, 227, 238, 248 };
|
||||
/* V */ static const uint8_t Font_Unifont_Bold_16_glyph_86[] = { 7, 10, 8, 0, 10, 199, 143, 26, 38, 205, 155, 20, 40, 112 };
|
||||
/* W */ static const uint8_t Font_Unifont_Bold_16_glyph_87[] = { 7, 10, 8, 0, 10, 199, 143, 30, 189, 122, 245, 255, 238, 136 };
|
||||
/* X */ static const uint8_t Font_Unifont_Bold_16_glyph_88[] = { 7, 10, 8, 0, 10, 199, 141, 179, 99, 135, 27, 54, 199, 140 };
|
||||
/* Y */ static const uint8_t Font_Unifont_Bold_16_glyph_89[] = { 6, 10, 7, 0, 10, 207, 60, 243, 73, 227, 12, 48, 192 };
|
||||
/* Z */ static const uint8_t Font_Unifont_Bold_16_glyph_90[] = { 7, 10, 8, 0, 10, 254, 12, 56, 225, 135, 28, 112, 193, 252 };
|
||||
/* [ */ static const uint8_t Font_Unifont_Bold_16_glyph_91[] = { 4, 12, 7, 2, 11, 252, 204, 204, 204, 204, 207, 0 };
|
||||
/* \ */ static const uint8_t Font_Unifont_Bold_16_glyph_92[] = { 6, 10, 7, 0, 10, 195, 6, 8, 48, 193, 6, 12, 48 };
|
||||
/* ] */ static const uint8_t Font_Unifont_Bold_16_glyph_93[] = { 4, 12, 7, 0, 11, 243, 51, 51, 51, 51, 63, 0 };
|
||||
/* ^ */ static const uint8_t Font_Unifont_Bold_16_glyph_94[] = { 7, 3, 8, 0, 12, 56, 219, 24 };
|
||||
/* _ */ static const uint8_t Font_Unifont_Bold_16_glyph_95[] = { 7, 1, 7, 0, 0, 254 };
|
||||
/* ` */ static const uint8_t Font_Unifont_Bold_16_glyph_96[] = { 4, 3, 7, 0, 13, 198, 48 };
|
||||
/* a */ static const uint8_t Font_Unifont_Bold_16_glyph_97[] = { 7, 8, 8, 0, 8, 125, 140, 27, 252, 120, 243, 187, 0 };
|
||||
/* b */ static const uint8_t Font_Unifont_Bold_16_glyph_98[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 207, 112 };
|
||||
/* c */ static const uint8_t Font_Unifont_Bold_16_glyph_99[] = { 7, 8, 8, 0, 8, 125, 143, 30, 12, 24, 241, 190, 0 };
|
||||
/* d */ static const uint8_t Font_Unifont_Bold_16_glyph_100[] = { 7, 11, 8, 0, 11, 6, 12, 25, 188, 248, 241, 227, 199, 156, 216 };
|
||||
/* e */ static const uint8_t Font_Unifont_Bold_16_glyph_101[] = { 7, 8, 8, 0, 8, 125, 143, 31, 252, 24, 241, 190, 0 };
|
||||
/* f */ static const uint8_t Font_Unifont_Bold_16_glyph_102[] = { 7, 11, 8, 0, 11, 30, 96, 193, 143, 230, 12, 24, 48, 97, 240 };
|
||||
/* g */ static const uint8_t Font_Unifont_Bold_16_glyph_103[] = { 7, 11, 8, 0, 9, 2, 247, 54, 108, 207, 8, 62, 207, 141, 240 };
|
||||
/* h */ static const uint8_t Font_Unifont_Bold_16_glyph_104[] = { 7, 11, 8, 0, 11, 193, 131, 6, 238, 120, 241, 227, 199, 143, 24 };
|
||||
/* i */ static const uint8_t Font_Unifont_Bold_16_glyph_105[] = { 6, 11, 7, 0, 11, 48, 192, 60, 48, 195, 12, 48, 207, 192 };
|
||||
/* j */ static const uint8_t Font_Unifont_Bold_16_glyph_106[] = { 6, 13, 8, 0, 11, 24, 96, 31, 12, 48, 195, 12, 60, 246, 112 };
|
||||
/* k */ static const uint8_t Font_Unifont_Bold_16_glyph_107[] = { 7, 11, 8, 0, 11, 193, 131, 6, 60, 251, 60, 120, 217, 159, 24 };
|
||||
/* l */ static const uint8_t Font_Unifont_Bold_16_glyph_108[] = { 6, 11, 7, 0, 11, 240, 195, 12, 48, 195, 12, 48, 207, 192 };
|
||||
/* m */ static const uint8_t Font_Unifont_Bold_16_glyph_109[] = { 7, 8, 8, 0, 8, 237, 175, 94, 189, 122, 245, 235, 0 };
|
||||
/* n */ static const uint8_t Font_Unifont_Bold_16_glyph_110[] = { 7, 8, 8, 0, 8, 221, 207, 30, 60, 120, 241, 227, 0 };
|
||||
/* o */ static const uint8_t Font_Unifont_Bold_16_glyph_111[] = { 7, 8, 8, 0, 8, 125, 143, 30, 60, 120, 241, 190, 0 };
|
||||
/* p */ static const uint8_t Font_Unifont_Bold_16_glyph_112[] = { 7, 10, 8, 0, 8, 221, 207, 30, 60, 120, 249, 238, 193, 128 };
|
||||
/* q */ static const uint8_t Font_Unifont_Bold_16_glyph_113[] = { 7, 10, 8, 0, 8, 119, 159, 30, 60, 120, 243, 187, 6, 12 };
|
||||
/* r */ static const uint8_t Font_Unifont_Bold_16_glyph_114[] = { 7, 8, 8, 0, 8, 221, 207, 30, 12, 24, 48, 96, 0 };
|
||||
/* s */ static const uint8_t Font_Unifont_Bold_16_glyph_115[] = { 7, 8, 8, 0, 8, 125, 143, 27, 129, 216, 241, 190, 0 };
|
||||
/* t */ static const uint8_t Font_Unifont_Bold_16_glyph_116[] = { 7, 10, 8, 0, 10, 48, 96, 199, 243, 6, 12, 24, 48, 60 };
|
||||
/* u */ static const uint8_t Font_Unifont_Bold_16_glyph_117[] = { 7, 8, 8, 0, 8, 199, 143, 30, 60, 120, 243, 187, 0 };
|
||||
/* v */ static const uint8_t Font_Unifont_Bold_16_glyph_118[] = { 7, 8, 8, 0, 8, 199, 143, 26, 38, 205, 142, 28, 0 };
|
||||
/* w */ static const uint8_t Font_Unifont_Bold_16_glyph_119[] = { 7, 8, 8, 0, 8, 199, 175, 94, 189, 122, 245, 182, 0 };
|
||||
/* x */ static const uint8_t Font_Unifont_Bold_16_glyph_120[] = { 7, 8, 8, 0, 8, 199, 141, 177, 195, 141, 177, 227, 0 };
|
||||
/* y */ static const uint8_t Font_Unifont_Bold_16_glyph_121[] = { 7, 10, 8, 0, 8, 199, 143, 30, 60, 109, 205, 131, 6, 248 };
|
||||
/* z */ static const uint8_t Font_Unifont_Bold_16_glyph_122[] = { 7, 8, 8, 0, 8, 254, 12, 56, 227, 142, 56, 127, 0 };
|
||||
/* { */ static const uint8_t Font_Unifont_Bold_16_glyph_123[] = { 5, 13, 7, 1, 11, 59, 24, 99, 51, 12, 49, 152, 195, 128 };
|
||||
/* | */ static const uint8_t Font_Unifont_Bold_16_glyph_124[] = { 2, 14, 7, 2, 12, 255, 255, 255, 240 };
|
||||
/* } */ static const uint8_t Font_Unifont_Bold_16_glyph_125[] = { 5, 13, 7, 0, 11, 225, 140, 198, 24, 102, 99, 12, 110, 0 };
|
||||
/* ~ */ static const uint8_t Font_Unifont_Bold_16_glyph_126[] = { 7, 3, 8, 0, 11, 99, 118, 48 };
|
||||
|
||||
const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[] = { 7, 10, 8, 0, 10, 130, 112, 231, 207, 60, 249, 255, 231, 207 };
|
||||
|
||||
const uint8_t * const Font_Unifont_Bold_16[126 + 1 - 32] = {
|
||||
Font_Unifont_Bold_16_glyph_32,
|
||||
Font_Unifont_Bold_16_glyph_33,
|
||||
Font_Unifont_Bold_16_glyph_34,
|
||||
Font_Unifont_Bold_16_glyph_35,
|
||||
Font_Unifont_Bold_16_glyph_36,
|
||||
Font_Unifont_Bold_16_glyph_37,
|
||||
Font_Unifont_Bold_16_glyph_38,
|
||||
Font_Unifont_Bold_16_glyph_39,
|
||||
Font_Unifont_Bold_16_glyph_40,
|
||||
Font_Unifont_Bold_16_glyph_41,
|
||||
Font_Unifont_Bold_16_glyph_42,
|
||||
Font_Unifont_Bold_16_glyph_43,
|
||||
Font_Unifont_Bold_16_glyph_44,
|
||||
Font_Unifont_Bold_16_glyph_45,
|
||||
Font_Unifont_Bold_16_glyph_46,
|
||||
Font_Unifont_Bold_16_glyph_47,
|
||||
Font_Unifont_Bold_16_glyph_48,
|
||||
Font_Unifont_Bold_16_glyph_49,
|
||||
Font_Unifont_Bold_16_glyph_50,
|
||||
Font_Unifont_Bold_16_glyph_51,
|
||||
Font_Unifont_Bold_16_glyph_52,
|
||||
Font_Unifont_Bold_16_glyph_53,
|
||||
Font_Unifont_Bold_16_glyph_54,
|
||||
Font_Unifont_Bold_16_glyph_55,
|
||||
Font_Unifont_Bold_16_glyph_56,
|
||||
Font_Unifont_Bold_16_glyph_57,
|
||||
Font_Unifont_Bold_16_glyph_58,
|
||||
Font_Unifont_Bold_16_glyph_59,
|
||||
Font_Unifont_Bold_16_glyph_60,
|
||||
Font_Unifont_Bold_16_glyph_61,
|
||||
Font_Unifont_Bold_16_glyph_62,
|
||||
Font_Unifont_Bold_16_glyph_63,
|
||||
Font_Unifont_Bold_16_glyph_64,
|
||||
Font_Unifont_Bold_16_glyph_65,
|
||||
Font_Unifont_Bold_16_glyph_66,
|
||||
Font_Unifont_Bold_16_glyph_67,
|
||||
Font_Unifont_Bold_16_glyph_68,
|
||||
Font_Unifont_Bold_16_glyph_69,
|
||||
Font_Unifont_Bold_16_glyph_70,
|
||||
Font_Unifont_Bold_16_glyph_71,
|
||||
Font_Unifont_Bold_16_glyph_72,
|
||||
Font_Unifont_Bold_16_glyph_73,
|
||||
Font_Unifont_Bold_16_glyph_74,
|
||||
Font_Unifont_Bold_16_glyph_75,
|
||||
Font_Unifont_Bold_16_glyph_76,
|
||||
Font_Unifont_Bold_16_glyph_77,
|
||||
Font_Unifont_Bold_16_glyph_78,
|
||||
Font_Unifont_Bold_16_glyph_79,
|
||||
Font_Unifont_Bold_16_glyph_80,
|
||||
Font_Unifont_Bold_16_glyph_81,
|
||||
Font_Unifont_Bold_16_glyph_82,
|
||||
Font_Unifont_Bold_16_glyph_83,
|
||||
Font_Unifont_Bold_16_glyph_84,
|
||||
Font_Unifont_Bold_16_glyph_85,
|
||||
Font_Unifont_Bold_16_glyph_86,
|
||||
Font_Unifont_Bold_16_glyph_87,
|
||||
Font_Unifont_Bold_16_glyph_88,
|
||||
Font_Unifont_Bold_16_glyph_89,
|
||||
Font_Unifont_Bold_16_glyph_90,
|
||||
Font_Unifont_Bold_16_glyph_91,
|
||||
Font_Unifont_Bold_16_glyph_92,
|
||||
Font_Unifont_Bold_16_glyph_93,
|
||||
Font_Unifont_Bold_16_glyph_94,
|
||||
Font_Unifont_Bold_16_glyph_95,
|
||||
Font_Unifont_Bold_16_glyph_96,
|
||||
Font_Unifont_Bold_16_glyph_97,
|
||||
Font_Unifont_Bold_16_glyph_98,
|
||||
Font_Unifont_Bold_16_glyph_99,
|
||||
Font_Unifont_Bold_16_glyph_100,
|
||||
Font_Unifont_Bold_16_glyph_101,
|
||||
Font_Unifont_Bold_16_glyph_102,
|
||||
Font_Unifont_Bold_16_glyph_103,
|
||||
Font_Unifont_Bold_16_glyph_104,
|
||||
Font_Unifont_Bold_16_glyph_105,
|
||||
Font_Unifont_Bold_16_glyph_106,
|
||||
Font_Unifont_Bold_16_glyph_107,
|
||||
Font_Unifont_Bold_16_glyph_108,
|
||||
Font_Unifont_Bold_16_glyph_109,
|
||||
Font_Unifont_Bold_16_glyph_110,
|
||||
Font_Unifont_Bold_16_glyph_111,
|
||||
Font_Unifont_Bold_16_glyph_112,
|
||||
Font_Unifont_Bold_16_glyph_113,
|
||||
Font_Unifont_Bold_16_glyph_114,
|
||||
Font_Unifont_Bold_16_glyph_115,
|
||||
Font_Unifont_Bold_16_glyph_116,
|
||||
Font_Unifont_Bold_16_glyph_117,
|
||||
Font_Unifont_Bold_16_glyph_118,
|
||||
Font_Unifont_Bold_16_glyph_119,
|
||||
Font_Unifont_Bold_16_glyph_120,
|
||||
Font_Unifont_Bold_16_glyph_121,
|
||||
Font_Unifont_Bold_16_glyph_122,
|
||||
Font_Unifont_Bold_16_glyph_123,
|
||||
Font_Unifont_Bold_16_glyph_124,
|
||||
Font_Unifont_Bold_16_glyph_125,
|
||||
Font_Unifont_Bold_16_glyph_126,
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#if TREZOR_FONT_BPP != 1
|
||||
#error Wrong TREZOR_FONT_BPP (expected 1)
|
||||
#endif
|
||||
extern const uint8_t* const Font_Unifont_Bold_16[126 + 1 - 32];
|
||||
extern const uint8_t Font_Unifont_Bold_16_glyph_nonprintable[];
|
@ -0,0 +1,203 @@
|
||||
#include <stdint.h>
|
||||
|
||||
// clang-format off
|
||||
|
||||
// - the first two bytes are width and height of the glyph
|
||||
// - the third, fourth and fifth bytes are advance, bearingX and bearingY of the horizontal metrics of the glyph
|
||||
// - the rest is packed 1-bit glyph data
|
||||
|
||||
/* */ static const uint8_t Font_Unifont_Regular_16_glyph_32[] = { 0, 0, 8, 0, 0 };
|
||||
/* ! */ static const uint8_t Font_Unifont_Regular_16_glyph_33[] = { 1, 10, 7, 3, 10, 254, 192 };
|
||||
/* " */ static const uint8_t Font_Unifont_Regular_16_glyph_34[] = { 5, 4, 7, 1, 12, 140, 99, 16 };
|
||||
/* # */ static const uint8_t Font_Unifont_Regular_16_glyph_35[] = { 6, 10, 7, 0, 10, 36, 146, 127, 73, 47, 228, 146, 64 };
|
||||
/* $ */ static const uint8_t Font_Unifont_Regular_16_glyph_36[] = { 7, 10, 7, 0, 10, 16, 250, 76, 135, 3, 132, 201, 124, 32 };
|
||||
/* % */ static const uint8_t Font_Unifont_Regular_16_glyph_37[] = { 7, 10, 7, 0, 10, 99, 42, 83, 65, 2, 11, 41, 83, 24 };
|
||||
/* & */ static const uint8_t Font_Unifont_Regular_16_glyph_38[] = { 7, 10, 7, 0, 10, 56, 137, 17, 67, 10, 98, 194, 140, 228 };
|
||||
/* ' */ static const uint8_t Font_Unifont_Regular_16_glyph_39[] = { 1, 4, 7, 3, 12, 240 };
|
||||
/* ( */ static const uint8_t Font_Unifont_Regular_16_glyph_40[] = { 3, 12, 7, 2, 11, 41, 73, 36, 137, 16 };
|
||||
/* ) */ static const uint8_t Font_Unifont_Regular_16_glyph_41[] = { 3, 12, 7, 1, 11, 137, 18, 73, 41, 64 };
|
||||
/* * */ static const uint8_t Font_Unifont_Regular_16_glyph_42[] = { 7, 7, 7, 0, 8, 17, 37, 81, 197, 82, 68, 0 };
|
||||
/* + */ static const uint8_t Font_Unifont_Regular_16_glyph_43[] = { 7, 7, 7, 0, 8, 16, 32, 71, 241, 2, 4, 0 };
|
||||
/* , */ static const uint8_t Font_Unifont_Regular_16_glyph_44[] = { 2, 4, 7, 2, 2, 214, 0 };
|
||||
/* - */ static const uint8_t Font_Unifont_Regular_16_glyph_45[] = { 4, 1, 7, 1, 5, 240 };
|
||||
/* . */ static const uint8_t Font_Unifont_Regular_16_glyph_46[] = { 2, 2, 7, 2, 2, 240 };
|
||||
/* / */ static const uint8_t Font_Unifont_Regular_16_glyph_47[] = { 6, 10, 7, 0, 10, 4, 16, 132, 16, 130, 16, 130, 0 };
|
||||
/* 0 */ static const uint8_t Font_Unifont_Regular_16_glyph_48[] = { 6, 10, 7, 0, 10, 49, 40, 99, 150, 156, 97, 72, 192 };
|
||||
/* 1 */ static const uint8_t Font_Unifont_Regular_16_glyph_49[] = { 5, 10, 7, 1, 10, 35, 40, 66, 16, 132, 39, 192 };
|
||||
/* 2 */ static const uint8_t Font_Unifont_Regular_16_glyph_50[] = { 6, 10, 7, 0, 10, 122, 24, 65, 24, 132, 32, 131, 240 };
|
||||
/* 3 */ static const uint8_t Font_Unifont_Regular_16_glyph_51[] = { 6, 10, 7, 0, 10, 122, 24, 65, 56, 16, 97, 133, 224 };
|
||||
/* 4 */ static const uint8_t Font_Unifont_Regular_16_glyph_52[] = { 6, 10, 7, 0, 10, 8, 98, 146, 138, 47, 194, 8, 32 };
|
||||
/* 5 */ static const uint8_t Font_Unifont_Regular_16_glyph_53[] = { 6, 10, 7, 0, 10, 254, 8, 32, 248, 16, 65, 133, 224 };
|
||||
/* 6 */ static const uint8_t Font_Unifont_Regular_16_glyph_54[] = { 6, 10, 7, 0, 10, 57, 8, 32, 250, 24, 97, 133, 224 };
|
||||
/* 7 */ static const uint8_t Font_Unifont_Regular_16_glyph_55[] = { 6, 10, 7, 0, 10, 252, 16, 66, 8, 33, 4, 16, 64 };
|
||||
/* 8 */ static const uint8_t Font_Unifont_Regular_16_glyph_56[] = { 6, 10, 7, 0, 10, 122, 24, 97, 122, 24, 97, 133, 224 };
|
||||
/* 9 */ static const uint8_t Font_Unifont_Regular_16_glyph_57[] = { 6, 10, 7, 0, 10, 122, 24, 97, 124, 16, 65, 9, 192 };
|
||||
/* : */ static const uint8_t Font_Unifont_Regular_16_glyph_58[] = { 2, 7, 7, 2, 8, 240, 60 };
|
||||
/* ; */ static const uint8_t Font_Unifont_Regular_16_glyph_59[] = { 2, 9, 7, 2, 8, 240, 53, 128 };
|
||||
/* < */ static const uint8_t Font_Unifont_Regular_16_glyph_60[] = { 5, 9, 7, 1, 9, 8, 136, 136, 32, 130, 8 };
|
||||
/* = */ static const uint8_t Font_Unifont_Regular_16_glyph_61[] = { 6, 5, 7, 0, 7, 252, 0, 0, 252 };
|
||||
/* > */ static const uint8_t Font_Unifont_Regular_16_glyph_62[] = { 5, 9, 7, 0, 9, 130, 8, 32, 136, 136, 128 };
|
||||
/* ? */ static const uint8_t Font_Unifont_Regular_16_glyph_63[] = { 6, 10, 7, 0, 10, 122, 24, 65, 8, 65, 0, 16, 64 };
|
||||
/* @ */ static const uint8_t Font_Unifont_Regular_16_glyph_64[] = { 6, 10, 7, 0, 10, 57, 25, 107, 166, 154, 103, 64, 240 };
|
||||
/* A */ static const uint8_t Font_Unifont_Regular_16_glyph_65[] = { 6, 10, 7, 0, 10, 49, 36, 161, 135, 248, 97, 134, 16 };
|
||||
/* B */ static const uint8_t Font_Unifont_Regular_16_glyph_66[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 24, 97, 135, 224 };
|
||||
/* C */ static const uint8_t Font_Unifont_Regular_16_glyph_67[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 8, 33, 133, 224 };
|
||||
/* D */ static const uint8_t Font_Unifont_Regular_16_glyph_68[] = { 6, 10, 7, 0, 10, 242, 40, 97, 134, 24, 97, 139, 192 };
|
||||
/* E */ static const uint8_t Font_Unifont_Regular_16_glyph_69[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 131, 240 };
|
||||
/* F */ static const uint8_t Font_Unifont_Regular_16_glyph_70[] = { 6, 10, 7, 0, 10, 254, 8, 32, 250, 8, 32, 130, 0 };
|
||||
/* G */ static const uint8_t Font_Unifont_Regular_16_glyph_71[] = { 6, 10, 7, 0, 10, 122, 24, 96, 130, 120, 97, 141, 208 };
|
||||
/* H */ static const uint8_t Font_Unifont_Regular_16_glyph_72[] = { 6, 10, 7, 0, 10, 134, 24, 97, 254, 24, 97, 134, 16 };
|
||||
/* I */ static const uint8_t Font_Unifont_Regular_16_glyph_73[] = { 5, 10, 7, 1, 10, 249, 8, 66, 16, 132, 39, 192 };
|
||||
/* J */ static const uint8_t Font_Unifont_Regular_16_glyph_74[] = { 7, 10, 7, 0, 10, 62, 16, 32, 64, 129, 2, 68, 136, 224 };
|
||||
/* K */ static const uint8_t Font_Unifont_Regular_16_glyph_75[] = { 6, 10, 7, 0, 10, 134, 41, 40, 195, 10, 36, 138, 16 };
|
||||
/* L */ static const uint8_t Font_Unifont_Regular_16_glyph_76[] = { 6, 10, 7, 0, 10, 130, 8, 32, 130, 8, 32, 131, 240 };
|
||||
/* M */ static const uint8_t Font_Unifont_Regular_16_glyph_77[] = { 6, 10, 7, 0, 10, 134, 28, 243, 182, 216, 97, 134, 16 };
|
||||
/* N */ static const uint8_t Font_Unifont_Regular_16_glyph_78[] = { 6, 10, 7, 0, 10, 135, 28, 105, 166, 89, 99, 142, 16 };
|
||||
/* O */ static const uint8_t Font_Unifont_Regular_16_glyph_79[] = { 6, 10, 7, 0, 10, 122, 24, 97, 134, 24, 97, 133, 224 };
|
||||
/* P */ static const uint8_t Font_Unifont_Regular_16_glyph_80[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 8, 32, 130, 0 };
|
||||
/* Q */ static const uint8_t Font_Unifont_Regular_16_glyph_81[] = { 7, 11, 7, 0, 10, 121, 10, 20, 40, 80, 161, 90, 204, 240, 24 };
|
||||
/* R */ static const uint8_t Font_Unifont_Regular_16_glyph_82[] = { 6, 10, 7, 0, 10, 250, 24, 97, 250, 72, 162, 134, 16 };
|
||||
/* S */ static const uint8_t Font_Unifont_Regular_16_glyph_83[] = { 6, 10, 7, 0, 10, 122, 24, 96, 96, 96, 97, 133, 224 };
|
||||
/* T */ static const uint8_t Font_Unifont_Regular_16_glyph_84[] = { 7, 10, 7, 0, 10, 254, 32, 64, 129, 2, 4, 8, 16, 32 };
|
||||
/* U */ static const uint8_t Font_Unifont_Regular_16_glyph_85[] = { 6, 10, 7, 0, 10, 134, 24, 97, 134, 24, 97, 133, 224 };
|
||||
/* V */ static const uint8_t Font_Unifont_Regular_16_glyph_86[] = { 7, 10, 7, 0, 10, 131, 6, 10, 36, 72, 138, 20, 16, 32 };
|
||||
/* W */ static const uint8_t Font_Unifont_Regular_16_glyph_87[] = { 6, 10, 7, 0, 10, 134, 24, 97, 182, 220, 243, 134, 16 };
|
||||
/* X */ static const uint8_t Font_Unifont_Regular_16_glyph_88[] = { 6, 10, 7, 0, 10, 134, 20, 146, 48, 196, 146, 134, 16 };
|
||||
/* Y */ static const uint8_t Font_Unifont_Regular_16_glyph_89[] = { 7, 10, 7, 0, 10, 131, 5, 18, 34, 130, 4, 8, 16, 32 };
|
||||
/* Z */ static const uint8_t Font_Unifont_Regular_16_glyph_90[] = { 6, 10, 7, 0, 10, 252, 16, 66, 16, 132, 32, 131, 240 };
|
||||
/* [ */ static const uint8_t Font_Unifont_Regular_16_glyph_91[] = { 3, 12, 7, 3, 11, 242, 73, 36, 146, 112 };
|
||||
/* \ */ static const uint8_t Font_Unifont_Regular_16_glyph_92[] = { 6, 10, 7, 0, 10, 130, 4, 8, 32, 65, 2, 4, 16 };
|
||||
/* ] */ static const uint8_t Font_Unifont_Regular_16_glyph_93[] = { 3, 12, 7, 0, 11, 228, 146, 73, 36, 240 };
|
||||
/* ^ */ static const uint8_t Font_Unifont_Regular_16_glyph_94[] = { 6, 3, 7, 0, 12, 49, 40, 64 };
|
||||
/* _ */ static const uint8_t Font_Unifont_Regular_16_glyph_95[] = { 7, 1, 7, 0, 0, 254 };
|
||||
/* ` */ static const uint8_t Font_Unifont_Regular_16_glyph_96[] = { 3, 3, 7, 1, 13, 136, 128 };
|
||||
/* a */ static const uint8_t Font_Unifont_Regular_16_glyph_97[] = { 6, 8, 7, 0, 8, 122, 16, 95, 134, 24, 221, 0 };
|
||||
/* b */ static const uint8_t Font_Unifont_Regular_16_glyph_98[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 135, 27, 128 };
|
||||
/* c */ static const uint8_t Font_Unifont_Regular_16_glyph_99[] = { 6, 8, 7, 0, 8, 122, 24, 32, 130, 8, 94, 0 };
|
||||
/* d */ static const uint8_t Font_Unifont_Regular_16_glyph_100[] = { 6, 11, 7, 0, 11, 4, 16, 93, 142, 24, 97, 134, 55, 64 };
|
||||
/* e */ static const uint8_t Font_Unifont_Regular_16_glyph_101[] = { 6, 8, 7, 0, 8, 122, 24, 127, 130, 8, 94, 0 };
|
||||
/* f */ static const uint8_t Font_Unifont_Regular_16_glyph_102[] = { 5, 11, 7, 0, 11, 25, 8, 79, 144, 132, 33, 8 };
|
||||
/* g */ static const uint8_t Font_Unifont_Regular_16_glyph_103[] = { 6, 11, 7, 0, 9, 5, 216, 162, 137, 196, 30, 134, 23, 128 };
|
||||
/* h */ static const uint8_t Font_Unifont_Regular_16_glyph_104[] = { 6, 11, 7, 0, 11, 130, 8, 46, 198, 24, 97, 134, 24, 64 };
|
||||
/* i */ static const uint8_t Font_Unifont_Regular_16_glyph_105[] = { 5, 11, 7, 1, 11, 33, 0, 194, 16, 132, 33, 62 };
|
||||
/* j */ static const uint8_t Font_Unifont_Regular_16_glyph_106[] = { 5, 13, 7, 0, 11, 8, 64, 48, 132, 33, 8, 67, 38, 0 };
|
||||
/* k */ static const uint8_t Font_Unifont_Regular_16_glyph_107[] = { 6, 11, 7, 0, 11, 130, 8, 34, 146, 140, 40, 146, 40, 64 };
|
||||
/* l */ static const uint8_t Font_Unifont_Regular_16_glyph_108[] = { 5, 11, 7, 1, 11, 97, 8, 66, 16, 132, 33, 62 };
|
||||
/* m */ static const uint8_t Font_Unifont_Regular_16_glyph_109[] = { 7, 8, 7, 0, 8, 237, 38, 76, 153, 50, 100, 201, 0 };
|
||||
/* n */ static const uint8_t Font_Unifont_Regular_16_glyph_110[] = { 6, 8, 7, 0, 8, 187, 24, 97, 134, 24, 97, 0 };
|
||||
/* o */ static const uint8_t Font_Unifont_Regular_16_glyph_111[] = { 6, 8, 7, 0, 8, 122, 24, 97, 134, 24, 94, 0 };
|
||||
/* p */ static const uint8_t Font_Unifont_Regular_16_glyph_112[] = { 6, 10, 7, 0, 8, 187, 24, 97, 134, 28, 110, 130, 0 };
|
||||
/* q */ static const uint8_t Font_Unifont_Regular_16_glyph_113[] = { 6, 10, 7, 0, 8, 118, 56, 97, 134, 24, 221, 4, 16 };
|
||||
/* r */ static const uint8_t Font_Unifont_Regular_16_glyph_114[] = { 6, 8, 7, 0, 8, 187, 24, 96, 130, 8, 32, 0 };
|
||||
/* s */ static const uint8_t Font_Unifont_Regular_16_glyph_115[] = { 6, 8, 7, 0, 8, 122, 24, 24, 24, 24, 94, 0 };
|
||||
/* t */ static const uint8_t Font_Unifont_Regular_16_glyph_116[] = { 5, 10, 7, 0, 10, 33, 9, 242, 16, 132, 32, 192 };
|
||||
/* u */ static const uint8_t Font_Unifont_Regular_16_glyph_117[] = { 6, 8, 7, 0, 8, 134, 24, 97, 134, 24, 221, 0 };
|
||||
/* v */ static const uint8_t Font_Unifont_Regular_16_glyph_118[] = { 6, 8, 7, 0, 8, 134, 24, 82, 73, 35, 12, 0 };
|
||||
/* w */ static const uint8_t Font_Unifont_Regular_16_glyph_119[] = { 7, 8, 7, 0, 8, 131, 38, 76, 153, 50, 100, 182, 0 };
|
||||
/* x */ static const uint8_t Font_Unifont_Regular_16_glyph_120[] = { 6, 8, 7, 0, 8, 134, 20, 140, 49, 40, 97, 0 };
|
||||
/* y */ static const uint8_t Font_Unifont_Regular_16_glyph_121[] = { 6, 10, 7, 0, 8, 134, 24, 97, 133, 51, 65, 5, 224 };
|
||||
/* z */ static const uint8_t Font_Unifont_Regular_16_glyph_122[] = { 6, 8, 7, 0, 8, 252, 16, 132, 33, 8, 63, 0 };
|
||||
/* { */ static const uint8_t Font_Unifont_Regular_16_glyph_123[] = { 4, 13, 7, 1, 11, 52, 66, 36, 132, 34, 68, 48 };
|
||||
/* | */ static const uint8_t Font_Unifont_Regular_16_glyph_124[] = { 1, 14, 7, 3, 12, 255, 252 };
|
||||
/* } */ static const uint8_t Font_Unifont_Regular_16_glyph_125[] = { 4, 13, 7, 1, 11, 194, 36, 66, 18, 68, 34, 192 };
|
||||
/* ~ */ static const uint8_t Font_Unifont_Regular_16_glyph_126[] = { 7, 3, 7, 0, 11, 99, 38, 48 };
|
||||
|
||||
const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[] = { 6, 10, 7, 0, 10, 133, 231, 190, 247, 190, 255, 239, 191 };
|
||||
|
||||
const uint8_t * const Font_Unifont_Regular_16[126 + 1 - 32] = {
|
||||
Font_Unifont_Regular_16_glyph_32,
|
||||
Font_Unifont_Regular_16_glyph_33,
|
||||
Font_Unifont_Regular_16_glyph_34,
|
||||
Font_Unifont_Regular_16_glyph_35,
|
||||
Font_Unifont_Regular_16_glyph_36,
|
||||
Font_Unifont_Regular_16_glyph_37,
|
||||
Font_Unifont_Regular_16_glyph_38,
|
||||
Font_Unifont_Regular_16_glyph_39,
|
||||
Font_Unifont_Regular_16_glyph_40,
|
||||
Font_Unifont_Regular_16_glyph_41,
|
||||
Font_Unifont_Regular_16_glyph_42,
|
||||
Font_Unifont_Regular_16_glyph_43,
|
||||
Font_Unifont_Regular_16_glyph_44,
|
||||
Font_Unifont_Regular_16_glyph_45,
|
||||
Font_Unifont_Regular_16_glyph_46,
|
||||
Font_Unifont_Regular_16_glyph_47,
|
||||
Font_Unifont_Regular_16_glyph_48,
|
||||
Font_Unifont_Regular_16_glyph_49,
|
||||
Font_Unifont_Regular_16_glyph_50,
|
||||
Font_Unifont_Regular_16_glyph_51,
|
||||
Font_Unifont_Regular_16_glyph_52,
|
||||
Font_Unifont_Regular_16_glyph_53,
|
||||
Font_Unifont_Regular_16_glyph_54,
|
||||
Font_Unifont_Regular_16_glyph_55,
|
||||
Font_Unifont_Regular_16_glyph_56,
|
||||
Font_Unifont_Regular_16_glyph_57,
|
||||
Font_Unifont_Regular_16_glyph_58,
|
||||
Font_Unifont_Regular_16_glyph_59,
|
||||
Font_Unifont_Regular_16_glyph_60,
|
||||
Font_Unifont_Regular_16_glyph_61,
|
||||
Font_Unifont_Regular_16_glyph_62,
|
||||
Font_Unifont_Regular_16_glyph_63,
|
||||
Font_Unifont_Regular_16_glyph_64,
|
||||
Font_Unifont_Regular_16_glyph_65,
|
||||
Font_Unifont_Regular_16_glyph_66,
|
||||
Font_Unifont_Regular_16_glyph_67,
|
||||
Font_Unifont_Regular_16_glyph_68,
|
||||
Font_Unifont_Regular_16_glyph_69,
|
||||
Font_Unifont_Regular_16_glyph_70,
|
||||
Font_Unifont_Regular_16_glyph_71,
|
||||
Font_Unifont_Regular_16_glyph_72,
|
||||
Font_Unifont_Regular_16_glyph_73,
|
||||
Font_Unifont_Regular_16_glyph_74,
|
||||
Font_Unifont_Regular_16_glyph_75,
|
||||
Font_Unifont_Regular_16_glyph_76,
|
||||
Font_Unifont_Regular_16_glyph_77,
|
||||
Font_Unifont_Regular_16_glyph_78,
|
||||
Font_Unifont_Regular_16_glyph_79,
|
||||
Font_Unifont_Regular_16_glyph_80,
|
||||
Font_Unifont_Regular_16_glyph_81,
|
||||
Font_Unifont_Regular_16_glyph_82,
|
||||
Font_Unifont_Regular_16_glyph_83,
|
||||
Font_Unifont_Regular_16_glyph_84,
|
||||
Font_Unifont_Regular_16_glyph_85,
|
||||
Font_Unifont_Regular_16_glyph_86,
|
||||
Font_Unifont_Regular_16_glyph_87,
|
||||
Font_Unifont_Regular_16_glyph_88,
|
||||
Font_Unifont_Regular_16_glyph_89,
|
||||
Font_Unifont_Regular_16_glyph_90,
|
||||
Font_Unifont_Regular_16_glyph_91,
|
||||
Font_Unifont_Regular_16_glyph_92,
|
||||
Font_Unifont_Regular_16_glyph_93,
|
||||
Font_Unifont_Regular_16_glyph_94,
|
||||
Font_Unifont_Regular_16_glyph_95,
|
||||
Font_Unifont_Regular_16_glyph_96,
|
||||
Font_Unifont_Regular_16_glyph_97,
|
||||
Font_Unifont_Regular_16_glyph_98,
|
||||
Font_Unifont_Regular_16_glyph_99,
|
||||
Font_Unifont_Regular_16_glyph_100,
|
||||
Font_Unifont_Regular_16_glyph_101,
|
||||
Font_Unifont_Regular_16_glyph_102,
|
||||
Font_Unifont_Regular_16_glyph_103,
|
||||
Font_Unifont_Regular_16_glyph_104,
|
||||
Font_Unifont_Regular_16_glyph_105,
|
||||
Font_Unifont_Regular_16_glyph_106,
|
||||
Font_Unifont_Regular_16_glyph_107,
|
||||
Font_Unifont_Regular_16_glyph_108,
|
||||
Font_Unifont_Regular_16_glyph_109,
|
||||
Font_Unifont_Regular_16_glyph_110,
|
||||
Font_Unifont_Regular_16_glyph_111,
|
||||
Font_Unifont_Regular_16_glyph_112,
|
||||
Font_Unifont_Regular_16_glyph_113,
|
||||
Font_Unifont_Regular_16_glyph_114,
|
||||
Font_Unifont_Regular_16_glyph_115,
|
||||
Font_Unifont_Regular_16_glyph_116,
|
||||
Font_Unifont_Regular_16_glyph_117,
|
||||
Font_Unifont_Regular_16_glyph_118,
|
||||
Font_Unifont_Regular_16_glyph_119,
|
||||
Font_Unifont_Regular_16_glyph_120,
|
||||
Font_Unifont_Regular_16_glyph_121,
|
||||
Font_Unifont_Regular_16_glyph_122,
|
||||
Font_Unifont_Regular_16_glyph_123,
|
||||
Font_Unifont_Regular_16_glyph_124,
|
||||
Font_Unifont_Regular_16_glyph_125,
|
||||
Font_Unifont_Regular_16_glyph_126,
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#if TREZOR_FONT_BPP != 1
|
||||
#error Wrong TREZOR_FONT_BPP (expected 1)
|
||||
#endif
|
||||
extern const uint8_t* const Font_Unifont_Regular_16[126 + 1 - 32];
|
||||
extern const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[];
|
@ -0,0 +1,105 @@
|
||||
use crate::ui::{component::EventCtx, util::ResultExt};
|
||||
use heapless::String;
|
||||
|
||||
/// Reified editing operations of `TextBox`.
|
||||
///
|
||||
/// Note: This does not contain all supported editing operations, only the ones
|
||||
/// we currently use.
|
||||
pub enum TextEdit {
|
||||
ReplaceLast(char),
|
||||
Append(char),
|
||||
}
|
||||
|
||||
/// Wraps a character buffer of maximum length `L` and provides text editing
|
||||
/// operations over it. Text ops usually take a `EventCtx` to request a paint
|
||||
/// pass in case of any state modification.
|
||||
pub struct TextBox<const L: usize> {
|
||||
text: String<L>,
|
||||
}
|
||||
|
||||
impl<const L: usize> TextBox<L> {
|
||||
/// Create a new `TextBox` with content `text`.
|
||||
pub fn new(text: String<L>) -> Self {
|
||||
Self { text }
|
||||
}
|
||||
|
||||
/// Create an empty `TextBox`.
|
||||
pub fn empty() -> Self {
|
||||
Self::new(String::new())
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.text.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.text.is_empty()
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.text.len() == self.text.capacity()
|
||||
}
|
||||
|
||||
/// Delete the last character of content, if any.
|
||||
pub fn delete_last(&mut self, ctx: &mut EventCtx) {
|
||||
let changed = self.text.pop().is_some();
|
||||
if changed {
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the last character of the content with `ch`. If the content is
|
||||
/// empty, `ch` is appended.
|
||||
pub fn replace_last(&mut self, ctx: &mut EventCtx, ch: char) {
|
||||
let previous = self.text.pop();
|
||||
self.text
|
||||
.push(ch)
|
||||
.assert_if_debugging_ui("TextBox has zero capacity");
|
||||
let changed = previous != Some(ch);
|
||||
if changed {
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Append `ch` at the end of the content.
|
||||
pub fn append(&mut self, ctx: &mut EventCtx, ch: char) {
|
||||
self.text.push(ch).assert_if_debugging_ui("TextBox is full");
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Append `slice` at the end of the content.
|
||||
pub fn append_slice(&mut self, ctx: &mut EventCtx, slice: &str) {
|
||||
self.text
|
||||
.push_str(slice)
|
||||
.assert_if_debugging_ui("TextBox is full");
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Replace the textbox content with `text`.
|
||||
pub fn replace(&mut self, ctx: &mut EventCtx, text: &str) {
|
||||
if self.text != text {
|
||||
self.text.clear();
|
||||
self.text
|
||||
.push_str(text)
|
||||
.assert_if_debugging_ui("TextBox is full");
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the textbox content.
|
||||
pub fn clear(&mut self, ctx: &mut EventCtx) {
|
||||
self.replace(ctx, "");
|
||||
}
|
||||
|
||||
/// Apply a editing operation to the text buffer.
|
||||
pub fn apply(&mut self, ctx: &mut EventCtx, edit: TextEdit) {
|
||||
match edit {
|
||||
TextEdit::ReplaceLast(char) => self.replace_last(ctx, char),
|
||||
TextEdit::Append(char) => self.append(ctx, char),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,258 @@
|
||||
//! Including some useful debugging features,
|
||||
//! like printing of the struct details.
|
||||
|
||||
use heapless::String;
|
||||
|
||||
use super::{
|
||||
component::{
|
||||
pad::Pad,
|
||||
text::{
|
||||
common::TextBox,
|
||||
layout::{Span, TextLayout},
|
||||
},
|
||||
},
|
||||
display::{Color, Font, Icon},
|
||||
geometry::{Grid, Insets, Offset, Point, Rect},
|
||||
};
|
||||
use crate::{micropython::buffer::StrBuffer, time::Duration};
|
||||
|
||||
#[cfg(feature = "model_tr")]
|
||||
use super::model_tr::component::ButtonDetails;
|
||||
|
||||
// NOTE: not defining a common trait, like
|
||||
// Debug {fn print(&self);}, so that the trait does
|
||||
// not need to be imported when using the
|
||||
// print() function. It suits the use-case of being quickly
|
||||
// able to use the print() for debugging and then delete it.
|
||||
|
||||
/// TODO: find out how much storage these functions take
|
||||
/// and probably hide them behind debug feature
|
||||
|
||||
impl StrBuffer {
|
||||
pub fn print(&self) {
|
||||
println!("StrBuffer:: ", self.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
impl Duration {
|
||||
pub fn print(&self) {
|
||||
println!("Duration:: ", inttostr!(self.to_millis()));
|
||||
}
|
||||
}
|
||||
|
||||
impl Point {
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Point:: ",
|
||||
"x: ",
|
||||
inttostr!(self.x),
|
||||
", y: ",
|
||||
inttostr!(self.y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn print(&self) {
|
||||
print!("Rect:: ");
|
||||
println!(&self.corners_points());
|
||||
}
|
||||
|
||||
pub fn corners_points(&self) -> String<30> {
|
||||
build_string!(
|
||||
30,
|
||||
"(",
|
||||
inttostr!(self.x0),
|
||||
",",
|
||||
inttostr!(self.y0),
|
||||
"), (",
|
||||
inttostr!(self.x1),
|
||||
",",
|
||||
inttostr!(self.y1),
|
||||
")"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Rect {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Rect");
|
||||
t.string(&self.corners_points());
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Color:: ",
|
||||
"R: ",
|
||||
inttostr!(self.r()),
|
||||
", G: ",
|
||||
inttostr!(self.g()),
|
||||
", B: ",
|
||||
inttostr!(self.b())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn print(&self) {
|
||||
println!("Font:: ", "text_height: ", inttostr!(self.text_height()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "model_tr")]
|
||||
impl<T: Clone + AsRef<str>> ButtonDetails<T> {
|
||||
pub fn print(&self) {
|
||||
let text: String<20> = if let Some(text) = self.text.clone() {
|
||||
text.as_ref().into()
|
||||
} else {
|
||||
"None".into()
|
||||
};
|
||||
let icon_text: String<20> = if let Some(icon) = &self.icon {
|
||||
icon.text.into()
|
||||
} else {
|
||||
"None".into()
|
||||
};
|
||||
let force_width: String<20> = if let Some(force_width) = self.force_width {
|
||||
inttostr!(force_width).into()
|
||||
} else {
|
||||
"None".into()
|
||||
};
|
||||
println!(
|
||||
"ButtonDetails:: ",
|
||||
"text: ",
|
||||
text.as_ref(),
|
||||
", icon_text: ",
|
||||
icon_text.as_ref(),
|
||||
", with_outline: ",
|
||||
booltostr!(self.with_outline),
|
||||
", with_arms: ",
|
||||
booltostr!(self.with_arms),
|
||||
", force_width: ",
|
||||
force_width.as_ref()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Offset:: ",
|
||||
"x: ",
|
||||
inttostr!(self.x),
|
||||
", y: ",
|
||||
inttostr!(self.y)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Insets {
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Insets:: ",
|
||||
"top: ",
|
||||
inttostr!(self.top),
|
||||
", right: ",
|
||||
inttostr!(self.right),
|
||||
", bottom: ",
|
||||
inttostr!(self.bottom),
|
||||
", left: ",
|
||||
inttostr!(self.left)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub fn print(&self) {
|
||||
print!(
|
||||
"Grid:: ",
|
||||
"rows: ",
|
||||
inttostr!(self.rows as i32),
|
||||
", cols: ",
|
||||
inttostr!(self.cols as i32),
|
||||
", spacing: ",
|
||||
inttostr!(self.spacing as i32)
|
||||
);
|
||||
print!(", area: ");
|
||||
self.area.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn dimension_str(&self) -> String<10> {
|
||||
build_string!(
|
||||
10,
|
||||
inttostr!(self.width() as i32),
|
||||
"x",
|
||||
inttostr!(self.height() as i32)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn print(&self) {
|
||||
println!(
|
||||
"Icon:: ",
|
||||
"text: ",
|
||||
self.text,
|
||||
", width: ",
|
||||
inttostr!(self.width() as i32),
|
||||
", height: ",
|
||||
inttostr!(self.height() as i32)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Icon {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Icon");
|
||||
t.string(self.text);
|
||||
t.string(&self.dimension_str());
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
impl TextLayout {
|
||||
pub fn print(&self) {
|
||||
print!(
|
||||
"TextLayout:: ",
|
||||
"padding_top: ",
|
||||
inttostr!(self.padding_top as i32),
|
||||
", padding_bottom: ",
|
||||
inttostr!(self.padding_bottom as i32)
|
||||
);
|
||||
print!(", bounds: ");
|
||||
self.bounds.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn print(&self) {
|
||||
print!(
|
||||
"Span:: ",
|
||||
"length: ",
|
||||
inttostr!(self.length as i32),
|
||||
", skip_next_chars: ",
|
||||
inttostr!(self.skip_next_chars as i32),
|
||||
", insert_hyphen_before_line_break: ",
|
||||
booltostr!(self.insert_hyphen_before_line_break)
|
||||
);
|
||||
print!(", advance: ");
|
||||
self.advance.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
pub fn print(&self) {
|
||||
print!("Pad:: ", "area: ");
|
||||
self.area.print();
|
||||
}
|
||||
}
|
||||
|
||||
impl<const L: usize> TextBox<L> {
|
||||
pub fn print(&self) {
|
||||
println!("TextBox:: ", "content: ", self.content());
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
use crate::ui::lerp::Lerp;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Color(u16);
|
||||
|
||||
impl Color {
|
||||
pub const fn from_u16(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
let r = (r as u16 & 0xF8) << 8;
|
||||
let g = (g as u16 & 0xFC) << 3;
|
||||
let b = (b as u16 & 0xF8) >> 3;
|
||||
Self(r | g | b)
|
||||
}
|
||||
|
||||
pub const fn luminance(self) -> u32 {
|
||||
((self.r() as u32 * 299) / 1000)
|
||||
+ (self.g() as u32 * 587) / 1000
|
||||
+ (self.b() as u32 * 114) / 1000
|
||||
}
|
||||
|
||||
pub const fn r(self) -> u8 {
|
||||
(self.0 >> 8) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub const fn g(self) -> u8 {
|
||||
(self.0 >> 3) as u8 & 0xFC
|
||||
}
|
||||
|
||||
pub const fn b(self) -> u8 {
|
||||
(self.0 << 3) as u8 & 0xF8
|
||||
}
|
||||
|
||||
pub fn to_u16(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn hi_byte(self) -> u8 {
|
||||
(self.to_u16() >> 8) as u8
|
||||
}
|
||||
|
||||
pub fn lo_byte(self) -> u8 {
|
||||
(self.to_u16() & 0xFF) as u8
|
||||
}
|
||||
|
||||
pub fn negate(self) -> Self {
|
||||
Self(!self.0)
|
||||
}
|
||||
|
||||
pub const fn white() -> Self {
|
||||
Self::rgb(255, 255, 255)
|
||||
}
|
||||
|
||||
pub const fn black() -> Self {
|
||||
Self::rgb(0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Lerp for Color {
|
||||
fn lerp(a: Self, b: Self, t: f32) -> Self {
|
||||
let r = u8::lerp(a.r(), b.r(), t);
|
||||
let g = u8::lerp(a.g(), b.g(), t);
|
||||
let b = u8::lerp(a.b(), b.b(), t);
|
||||
Color::rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Color {
|
||||
fn from(val: u16) -> Self {
|
||||
Self(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for u16 {
|
||||
fn from(val: Color) -> Self {
|
||||
val.to_u16()
|
||||
}
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
use crate::{ui::{constant, geometry::{Point, Offset, Rect}}, trezorhal::display};
|
||||
use core::slice;
|
||||
|
||||
use super::{Color, get_color_table, pixeldata, set_window, get_offset};
|
||||
|
||||
|
||||
pub struct Glyph {
|
||||
pub width: i16,
|
||||
pub height: i16,
|
||||
pub adv: i16,
|
||||
pub bearing_x: i16,
|
||||
pub bearing_y: i16,
|
||||
data: &'static [u8],
|
||||
}
|
||||
|
||||
impl Glyph {
|
||||
/// Construct a `Glyph` from a raw pointer.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function is unsafe because the caller has to guarantee that `data`
|
||||
/// is pointing to a memory containing a valid glyph data, that is:
|
||||
/// - contains valid glyph metadata
|
||||
/// - data has appropriate size
|
||||
/// - data must have static lifetime
|
||||
pub unsafe fn load(data: *const u8) -> Self {
|
||||
unsafe {
|
||||
let width = *data.offset(0) as i16;
|
||||
let height = *data.offset(1) as i16;
|
||||
|
||||
let data_bits = constant::FONT_BPP * width * height;
|
||||
|
||||
let data_bytes = if data_bits % 8 == 0 {
|
||||
data_bits / 8
|
||||
} else {
|
||||
(data_bits / 8) + 1
|
||||
};
|
||||
|
||||
Glyph {
|
||||
width,
|
||||
height,
|
||||
adv: *data.offset(2) as i16,
|
||||
bearing_x: *data.offset(3) as i16,
|
||||
bearing_y: *data.offset(4) as i16,
|
||||
data: slice::from_raw_parts(data.offset(5), data_bytes as usize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn print(&self, pos: Point, colortable: [Color; 16]) -> i16 {
|
||||
let bearing = Offset::new(self.bearing_x, -self.bearing_y);
|
||||
let size = Offset::new(self.width, self.height);
|
||||
let pos_adj = pos + bearing;
|
||||
let r = Rect::from_top_left_and_size(pos_adj, size);
|
||||
|
||||
let area = r.translate(get_offset());
|
||||
let window = area.clamp(constant::screen());
|
||||
|
||||
set_window(window);
|
||||
|
||||
for y in window.y0..window.y1 {
|
||||
for x in window.x0..window.x1 {
|
||||
let p = Point::new(x, y);
|
||||
let r = p - pos_adj;
|
||||
let c = self.get_pixel_data(r);
|
||||
pixeldata(colortable[c as usize]);
|
||||
}
|
||||
}
|
||||
self.adv
|
||||
}
|
||||
|
||||
pub fn unpack_bpp1(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 8) as usize];
|
||||
((c_data >> (7 - (a % 8))) & 0x01) * 15
|
||||
}
|
||||
|
||||
pub fn unpack_bpp2(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 4) as usize];
|
||||
((c_data >> (6 - (a % 4) * 2)) & 0x03) * 5
|
||||
}
|
||||
|
||||
pub fn unpack_bpp4(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[(a / 2) as usize];
|
||||
(c_data >> (4 - (a % 2) * 4)) & 0x0F
|
||||
}
|
||||
|
||||
pub fn unpack_bpp8(&self, a: i16) -> u8 {
|
||||
let c_data = self.data[a as usize];
|
||||
c_data >> 4
|
||||
}
|
||||
|
||||
pub fn get_pixel_data(&self, p: Offset) -> u8 {
|
||||
let a = p.x + p.y * self.width;
|
||||
|
||||
match constant::FONT_BPP {
|
||||
1 => self.unpack_bpp1(a),
|
||||
2 => self.unpack_bpp2(a),
|
||||
4 => self.unpack_bpp4(a),
|
||||
8 => self.unpack_bpp8(a),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Font constants. Keep in sync with FONT_ definitions in
|
||||
/// `extmod/modtrezorui/fonts/fonts.h`.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, FromPrimitive)]
|
||||
#[repr(u8)]
|
||||
pub enum Font {
|
||||
NORMAL = 1,
|
||||
BOLD = 2,
|
||||
MONO = 3,
|
||||
DEMIBOLD = 5,
|
||||
}
|
||||
|
||||
impl From<Font> for i32 {
|
||||
fn from(font: Font) -> i32 {
|
||||
-(font as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
pub fn text_width(self, text: &str) -> i16 {
|
||||
display::text_width(text, self.into())
|
||||
}
|
||||
|
||||
pub fn char_width(self, ch: char) -> i16 {
|
||||
display::char_width(ch, self.into())
|
||||
}
|
||||
|
||||
pub fn text_height(self) -> i16 {
|
||||
display::text_height(self.into())
|
||||
}
|
||||
|
||||
pub fn text_max_height(self) -> i16 {
|
||||
display::text_max_height(self.into())
|
||||
}
|
||||
|
||||
pub fn text_baseline(self) -> i16 {
|
||||
display::text_baseline(self.into())
|
||||
}
|
||||
|
||||
pub fn line_height(self) -> i16 {
|
||||
constant::LINE_SPACE + self.text_height()
|
||||
}
|
||||
|
||||
pub fn get_glyph(self, char_byte: u8) -> Option<Glyph> {
|
||||
let gl_data = display::get_char_glyph(char_byte, self.into());
|
||||
|
||||
if gl_data.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { Some(Glyph::load(gl_data)) }
|
||||
}
|
||||
|
||||
pub fn display_text(self, text: &str, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let colortable = get_color_table(fg_color, bg_color);
|
||||
let mut adv_total = 0;
|
||||
for c in text.bytes() {
|
||||
let g = self.get_glyph(c);
|
||||
if let Some(gly) = g {
|
||||
let adv = gly.print(baseline + Offset::new(adv_total, 0), colortable);
|
||||
adv_total += adv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the length of the longest suffix from a given `text`
|
||||
/// that will fit into the area `width` pixels wide.
|
||||
pub fn longest_suffix(self, width: i16, text: &str) -> usize {
|
||||
let mut text_width = 0;
|
||||
for (chars_from_right, c) in text.chars().rev().enumerate() {
|
||||
let c_width = self.char_width(c);
|
||||
if text_width + c_width > width {
|
||||
// Another character cannot be fitted, we're done.
|
||||
return chars_from_right;
|
||||
}
|
||||
text_width += c_width;
|
||||
}
|
||||
|
||||
text.len() // it fits in its entirety
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
use crate::{
|
||||
trezorhal::display::ToifFormat,
|
||||
ui::geometry::{Offset, Point, Rect},
|
||||
};
|
||||
|
||||
use super::{icon_rect, toif_info_ensure, Color};
|
||||
|
||||
/// Storing the icon together with its name
|
||||
/// Needs to be a tuple-struct, so it can be made `const`
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct IconAndName(&'static [u8], &'static str);
|
||||
|
||||
impl IconAndName {
|
||||
pub const fn new(icon: &'static [u8], name: &'static str) -> Self {
|
||||
Self(icon, name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Holding icon data and allowing it to draw itself.
|
||||
/// Lots of draw methods exist so that we can easily
|
||||
/// "glue" the icon together with other elements
|
||||
/// (text, display boundary, etc.) according to their position.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Icon {
|
||||
pub data: &'static [u8],
|
||||
// Text is useful for debugging purposes.
|
||||
pub text: &'static str,
|
||||
// TODO: could include the info about "real" icon dimensions,
|
||||
// accounting for the TOIF limitations (when we sometimes
|
||||
// need to have empty row or column) - it could be
|
||||
// erasing those empty rows/columns when we draw the icon.
|
||||
}
|
||||
|
||||
// TODO: consider merging it together with ToifInfo
|
||||
impl Icon {
|
||||
pub fn new(icon_and_name: IconAndName) -> Self {
|
||||
Icon {
|
||||
data: icon_and_name.0,
|
||||
text: icon_and_name.1,
|
||||
}
|
||||
}
|
||||
|
||||
fn toif_info(&self) -> (Offset, &[u8]) {
|
||||
toif_info_ensure(self.data, ToifFormat::GrayScaleEH)
|
||||
}
|
||||
|
||||
pub fn width(&self) -> i16 {
|
||||
self.toif_info().0.x as i16
|
||||
}
|
||||
|
||||
pub fn height(&self) -> i16 {
|
||||
self.toif_info().0.y as i16
|
||||
}
|
||||
|
||||
pub fn toif_size(&self) -> Offset {
|
||||
self.toif_info().0
|
||||
}
|
||||
|
||||
pub fn toif_data(&self) -> &[u8] {
|
||||
self.toif_info().1
|
||||
}
|
||||
|
||||
/// Display icon at a specified Rectangle.
|
||||
fn draw_icon_rect(&self, r: Rect, fg_color: Color, bg_color: Color) {
|
||||
icon_rect(r, self.toif_data(), fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon with left top baseline Point.
|
||||
pub fn draw_top_left(&self, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_top_left_and_size(baseline, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon with right top baseline Point.
|
||||
pub fn draw_top_right(&self, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_top_right_and_size(baseline, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon with right bottom baseline Point.
|
||||
pub fn draw_bottom_right(&self, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_bottom_right_and_size(baseline, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon with left bottom baseline Point.
|
||||
pub fn draw_bottom_left(&self, baseline: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_bottom_left_and_size(baseline, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
|
||||
/// Display the icon around center Point.
|
||||
pub fn draw_center(&self, center: Point, fg_color: Color, bg_color: Color) {
|
||||
let r = Rect::from_center_and_size(center, self.toif_size());
|
||||
self.draw_icon_rect(r, fg_color, bg_color);
|
||||
}
|
||||
}
|
@ -0,0 +1,282 @@
|
||||
use crate::{
|
||||
trezorhal::bip39,
|
||||
ui::{
|
||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
|
||||
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, TextChoiceItem,
|
||||
};
|
||||
use heapless::{String, Vec};
|
||||
|
||||
pub enum Bip39EntryMsg {
|
||||
ResultWord(String<15>),
|
||||
}
|
||||
|
||||
const CURRENT_LETTERS_ROW: i32 = 25;
|
||||
|
||||
const MAX_LENGTH: usize = 10;
|
||||
const MAX_CHOICE_LENGTH: usize = 26;
|
||||
|
||||
/// Offer words when there will be fewer of them than this
|
||||
const OFFER_WORDS_THRESHOLD: usize = 10;
|
||||
|
||||
struct ChoiceFactoryBIP39 {
|
||||
// TODO: replace these Vecs by iterators somehow?
|
||||
letter_choices: Option<Vec<char, MAX_CHOICE_LENGTH>>,
|
||||
word_choices: Option<Vec<&'static str, OFFER_WORDS_THRESHOLD>>,
|
||||
}
|
||||
impl ChoiceFactoryBIP39 {
|
||||
fn new(
|
||||
letter_choices: Option<Vec<char, MAX_CHOICE_LENGTH>>,
|
||||
word_choices: Option<Vec<&'static str, OFFER_WORDS_THRESHOLD>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
letter_choices,
|
||||
word_choices,
|
||||
}
|
||||
}
|
||||
|
||||
fn letters(letter_choices: Vec<char, MAX_CHOICE_LENGTH>) -> Self {
|
||||
Self::new(Some(letter_choices), None)
|
||||
}
|
||||
|
||||
fn words(word_choices: Vec<&'static str, OFFER_WORDS_THRESHOLD>) -> Self {
|
||||
Self::new(None, Some(word_choices))
|
||||
}
|
||||
|
||||
/// Word choice items with BIN leftmost button.
|
||||
fn get_word_item(&self, choice_index: u8) -> ChoiceItem {
|
||||
if let Some(word_choices) = &self.word_choices {
|
||||
let word = word_choices[choice_index as usize];
|
||||
let choice = TextChoiceItem::new(word, ButtonLayout::default_three_icons());
|
||||
let mut word_item = ChoiceItem::Text(choice);
|
||||
|
||||
// Adding BIN leftmost button and removing the rightmost one.
|
||||
if choice_index == 0 {
|
||||
word_item.set_left_btn(Some(ButtonDetails::bin_icon()));
|
||||
} else if choice_index as usize == word_choices.len() - 1 {
|
||||
word_item.set_right_btn(None);
|
||||
}
|
||||
|
||||
word_item
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Letter choice items with BIN leftmost button. Letters are BIG.
|
||||
fn get_letter_item(&self, choice_index: u8) -> ChoiceItem {
|
||||
// TODO: we could support carousel for letters to quicken it for users
|
||||
// (but then the BIN would need to be an option on its own, not so
|
||||
// user-friendly)
|
||||
if let Some(letter_choices) = &self.letter_choices {
|
||||
let letter = letter_choices[choice_index as usize];
|
||||
let letter_choice =
|
||||
BigCharacterChoiceItem::new(letter, ButtonLayout::default_three_icons());
|
||||
let mut letter_item = ChoiceItem::BigCharacter(letter_choice);
|
||||
|
||||
// Adding BIN leftmost button and removing the rightmost one.
|
||||
if choice_index == 0 {
|
||||
letter_item.set_left_btn(Some(ButtonDetails::bin_icon()));
|
||||
} else if choice_index as usize == letter_choices.len() - 1 {
|
||||
letter_item.set_right_btn(None);
|
||||
}
|
||||
|
||||
letter_item
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl ChoiceFactory for ChoiceFactoryBIP39 {
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem {
|
||||
if self.letter_choices.is_some() {
|
||||
self.get_letter_item(choice_index)
|
||||
} else if self.word_choices.is_some() {
|
||||
self.get_word_item(choice_index)
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&self) -> u8 {
|
||||
if let Some(letter_choices) = &self.letter_choices {
|
||||
letter_choices.len() as u8
|
||||
} else if let Some(word_choices) = &self.word_choices {
|
||||
word_choices.len() as u8
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component for entering a BIP39 mnemonic.
|
||||
pub struct Bip39Entry {
|
||||
choice_page: ChoicePage<ChoiceFactoryBIP39>,
|
||||
chosen_letters: Child<ChangingTextLine<String<{ MAX_LENGTH + 1 }>>>,
|
||||
letter_choices: Vec<char, MAX_CHOICE_LENGTH>,
|
||||
textbox: TextBox<MAX_LENGTH>,
|
||||
offer_words: bool,
|
||||
words_list: bip39::Wordlist,
|
||||
}
|
||||
|
||||
impl Bip39Entry {
|
||||
pub fn new() -> Self {
|
||||
let letter_choices: Vec<char, MAX_CHOICE_LENGTH> =
|
||||
bip39::get_available_letters("").collect();
|
||||
let choices = ChoiceFactoryBIP39::letters(letter_choices.clone());
|
||||
|
||||
Self {
|
||||
choice_page: ChoicePage::new(choices),
|
||||
chosen_letters: Child::new(ChangingTextLine::center_mono(String::new())),
|
||||
letter_choices,
|
||||
textbox: TextBox::empty(),
|
||||
offer_words: false,
|
||||
words_list: bip39::Wordlist::all(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets up-to-date choices for letters or words.
|
||||
fn get_current_choices(&mut self) -> ChoiceFactoryBIP39 {
|
||||
// Narrowing the word list
|
||||
self.words_list = self.words_list.filter_prefix(self.textbox.content());
|
||||
|
||||
// Offering words when there is only a few of them
|
||||
// Otherwise getting relevant letters
|
||||
if self.words_list.len() < OFFER_WORDS_THRESHOLD {
|
||||
self.offer_words = true;
|
||||
let word_choices = self.words_list.iter().collect();
|
||||
ChoiceFactoryBIP39::words(word_choices)
|
||||
} else {
|
||||
self.offer_words = false;
|
||||
self.letter_choices = bip39::get_available_letters(self.textbox.content()).collect();
|
||||
ChoiceFactoryBIP39::letters(self.letter_choices.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn update_chosen_letters(&mut self, ctx: &mut EventCtx) {
|
||||
let text = build_string!({ MAX_LENGTH + 1 }, self.textbox.content(), "_");
|
||||
self.chosen_letters.inner_mut().update_text(text);
|
||||
self.chosen_letters.request_complete_repaint(ctx);
|
||||
}
|
||||
|
||||
fn append_letter(&mut self, ctx: &mut EventCtx, letter: char) {
|
||||
self.textbox.append(ctx, letter);
|
||||
}
|
||||
|
||||
fn delete_last_letter(&mut self, ctx: &mut EventCtx) {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
|
||||
fn reset_wordlist(&mut self) {
|
||||
self.words_list = bip39::Wordlist::all();
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Bip39Entry {
|
||||
type Msg = Bip39EntryMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let letters_area_height = self.chosen_letters.inner().needed_height();
|
||||
let (letters_area, choice_area) = bounds.split_top(letters_area_height);
|
||||
self.chosen_letters.place(letters_area);
|
||||
self.choice_page.place(choice_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
match msg {
|
||||
Some(ChoicePageMsg::Choice(page_counter)) => {
|
||||
// Clicked SELECT.
|
||||
// When we already offer words, return the word at the given index.
|
||||
// Otherwise, appending the new letter and resetting the choice page
|
||||
// with up-to-date choices.
|
||||
if self.offer_words {
|
||||
let word = self
|
||||
.words_list
|
||||
.get(page_counter as usize)
|
||||
.unwrap_or_default();
|
||||
return Some(Bip39EntryMsg::ResultWord(String::from(word)));
|
||||
} else {
|
||||
let new_letter = self.letter_choices[page_counter as usize];
|
||||
self.append_letter(ctx, new_letter);
|
||||
self.update_chosen_letters(ctx);
|
||||
let new_choices = self.get_current_choices();
|
||||
self.choice_page.reset(ctx, new_choices, true, false);
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
Some(ChoicePageMsg::LeftMost) => {
|
||||
// Clicked BIN. Deleting last letter, updating wordlist and updating choices
|
||||
self.delete_last_letter(ctx);
|
||||
self.update_chosen_letters(ctx);
|
||||
self.reset_wordlist();
|
||||
let new_choices = self.get_current_choices();
|
||||
self.choice_page.reset(ctx, new_choices, true, false);
|
||||
ctx.request_paint();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.chosen_letters.paint();
|
||||
self.choice_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::{ButtonAction, ButtonPos};
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use crate::ui::util;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for Bip39Entry {
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => match self.choice_page.has_previous_choice() {
|
||||
true => ButtonAction::PrevPage.string(),
|
||||
false => ButtonAction::Action("Delete last char").string(),
|
||||
},
|
||||
ButtonPos::Right => match self.choice_page.has_next_choice() {
|
||||
true => ButtonAction::NextPage.string(),
|
||||
false => ButtonAction::empty(),
|
||||
},
|
||||
ButtonPos::Middle => {
|
||||
let current_index = self.choice_page.page_index() as usize;
|
||||
let choice: String<10> = if self.offer_words {
|
||||
self.words_list
|
||||
.get(current_index)
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
} else {
|
||||
util::char_to_string(self.letter_choices[current_index])
|
||||
};
|
||||
ButtonAction::select_item(choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Bip39Entry");
|
||||
t.kw_pair("textbox", self.textbox.content());
|
||||
|
||||
self.report_btn_actions(t);
|
||||
|
||||
t.open("letter_choices");
|
||||
for ch in &self.letter_choices {
|
||||
t.string(&util::char_to_string::<1>(*ch));
|
||||
}
|
||||
t.close();
|
||||
|
||||
t.field("choice_page", &self.choice_page);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,449 @@
|
||||
use super::{
|
||||
theme, Button, ButtonDetails, ButtonLayout, ButtonPos, HoldToConfirm, HoldToConfirmMsg,
|
||||
LoaderStyleSheet,
|
||||
};
|
||||
use crate::{
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{base::Event, Component, EventCtx, Pad},
|
||||
event::{ButtonEvent, PhysicalButton},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::String;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum ButtonState {
|
||||
Nothing,
|
||||
OneDown(PhysicalButton),
|
||||
BothDown,
|
||||
OneReleased(PhysicalButton),
|
||||
HTCNeedsRelease(PhysicalButton),
|
||||
}
|
||||
|
||||
pub enum ButtonControllerMsg {
|
||||
Triggered(ButtonPos),
|
||||
}
|
||||
|
||||
/// Defines what kind of button should be currently used.
|
||||
pub enum ButtonType<T> {
|
||||
Button(Button<T>),
|
||||
HoldToConfirm(HoldToConfirm<T>),
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl<T> ButtonType<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
pub fn from_button_details(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
|
||||
if let Some(btn_details) = btn_details {
|
||||
if btn_details.duration.is_some() {
|
||||
Self::HoldToConfirm(Self::get_hold_to_confirm(pos, btn_details))
|
||||
} else {
|
||||
Self::Button(Self::get_button(pos, btn_details))
|
||||
}
|
||||
} else {
|
||||
Self::Nothing
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `Button` component from `btn_details`.
|
||||
fn get_button(pos: ButtonPos, btn_details: ButtonDetails<T>) -> Button<T> {
|
||||
// Deciding between text and icon
|
||||
if let Some(text) = btn_details.clone().text {
|
||||
Button::with_text(pos, text, btn_details.style())
|
||||
} else if let Some(icon) = btn_details.icon {
|
||||
Button::with_icon(pos, icon, btn_details.style())
|
||||
} else {
|
||||
panic!("ButtonContainer: no text or icon provided");
|
||||
}
|
||||
}
|
||||
|
||||
/// Create `HoldToConfirm` component from `btn_details`.
|
||||
fn get_hold_to_confirm(pos: ButtonPos, btn_details: ButtonDetails<T>) -> HoldToConfirm<T> {
|
||||
let duration = btn_details
|
||||
.duration
|
||||
.unwrap_or_else(|| Duration::from_millis(1000));
|
||||
if let Some(text) = btn_details.text {
|
||||
HoldToConfirm::text(pos, text, LoaderStyleSheet::default(), duration)
|
||||
} else if let Some(icon) = btn_details.icon {
|
||||
HoldToConfirm::icon(pos, icon, LoaderStyleSheet::default(), duration)
|
||||
} else {
|
||||
panic!("ButtonContainer: no text or icon provided");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place(&mut self, button_area: Rect) {
|
||||
match self {
|
||||
Self::Button(button) => {
|
||||
button.place(button_area);
|
||||
}
|
||||
Self::HoldToConfirm(htc) => {
|
||||
htc.place(button_area);
|
||||
}
|
||||
Self::Nothing => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paint(&mut self) {
|
||||
match self {
|
||||
Self::Button(button) => {
|
||||
button.paint();
|
||||
}
|
||||
Self::HoldToConfirm(htc) => {
|
||||
htc.paint();
|
||||
}
|
||||
Self::Nothing => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapping a button and its state, so that it can be easily
|
||||
/// controlled from outside.
|
||||
///
|
||||
/// Users have a choice of a normal button or Hold-to-confirm button.
|
||||
/// `button_type` specified what from those two is used, if anything.
|
||||
pub struct ButtonContainer<T> {
|
||||
pos: ButtonPos,
|
||||
button_type: ButtonType<T>,
|
||||
}
|
||||
|
||||
impl<T: Clone + AsRef<str>> ButtonContainer<T> {
|
||||
/// Supplying `None` as `btn_details` marks the button inactive
|
||||
/// (it can be later activated in `set()`).
|
||||
pub fn new(pos: ButtonPos, btn_details: Option<ButtonDetails<T>>) -> Self {
|
||||
Self {
|
||||
pos,
|
||||
button_type: ButtonType::from_button_details(pos, btn_details),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changing the state of the button.
|
||||
///
|
||||
/// Passing `None` as `btn_details` will mark the button as inactive.
|
||||
pub fn set(&mut self, btn_details: Option<ButtonDetails<T>>, button_area: Rect) {
|
||||
self.button_type = ButtonType::from_button_details(self.pos, btn_details);
|
||||
self.button_type.place(button_area);
|
||||
}
|
||||
|
||||
/// Placing the possible component.
|
||||
pub fn place(&mut self, bounds: Rect) {
|
||||
self.button_type.place(bounds);
|
||||
}
|
||||
|
||||
/// Painting the component that should be currently visible, if any.
|
||||
pub fn paint(&mut self) {
|
||||
self.button_type.paint();
|
||||
}
|
||||
|
||||
/// Setting the visual state of the button - released/pressed.
|
||||
pub fn set_pressed(&mut self, ctx: &mut EventCtx, is_pressed: bool) {
|
||||
if let ButtonType::Button(btn) = &mut self.button_type {
|
||||
btn.set_pressed(ctx, is_pressed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether single-click should trigger action.
|
||||
pub fn reacts_to_single_click(&self) -> bool {
|
||||
matches!(self.button_type, ButtonType::Button(_))
|
||||
}
|
||||
|
||||
/// Find out whether hold-to-confirm was triggered.
|
||||
pub fn htc_got_triggered(&mut self, ctx: &mut EventCtx, event: Event) -> bool {
|
||||
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
|
||||
if matches!(htc.event(ctx, event), Some(HoldToConfirmMsg::Confirmed)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Registering hold event.
|
||||
pub fn hold_started(&mut self, ctx: &mut EventCtx) {
|
||||
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
|
||||
htc.event(ctx, Event::Button(ButtonEvent::HoldStarted));
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancelling hold event.
|
||||
pub fn hold_ended(&mut self, ctx: &mut EventCtx) {
|
||||
if let ButtonType::HoldToConfirm(htc) = &mut self.button_type {
|
||||
htc.event(ctx, Event::Button(ButtonEvent::HoldEnded));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component responsible for handling buttons.
|
||||
///
|
||||
/// Acts as a state-machine of `ButtonState`.
|
||||
///
|
||||
/// Storing all three possible buttons - left, middle and right -
|
||||
/// and handling their placement, painting and returning
|
||||
/// appropriate events when they are triggered.
|
||||
///
|
||||
/// Buttons can be interactively changed by clients by `set()`.
|
||||
///
|
||||
/// Only "final" button events are returned in `ButtonControllerMsg::Triggered`,
|
||||
/// based upon the buttons being long-press or not.
|
||||
pub struct ButtonController<T> {
|
||||
pad: Pad,
|
||||
left_btn: ButtonContainer<T>,
|
||||
middle_btn: ButtonContainer<T>,
|
||||
right_btn: ButtonContainer<T>,
|
||||
state: ButtonState,
|
||||
// Button area is needed so the buttons
|
||||
// can be "re-placed" after their text is changed
|
||||
// Will be set in `place`
|
||||
button_area: Rect,
|
||||
}
|
||||
|
||||
impl<T: Clone + AsRef<str>> ButtonController<T> {
|
||||
pub fn new(btn_layout: ButtonLayout<T>) -> Self {
|
||||
Self {
|
||||
pad: Pad::with_background(theme::BG).with_clear(),
|
||||
left_btn: ButtonContainer::new(ButtonPos::Left, btn_layout.btn_left),
|
||||
middle_btn: ButtonContainer::new(ButtonPos::Middle, btn_layout.btn_middle),
|
||||
right_btn: ButtonContainer::new(ButtonPos::Right, btn_layout.btn_right),
|
||||
state: ButtonState::Nothing,
|
||||
button_area: Rect::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updating all the three buttons to the wanted states.
|
||||
pub fn set(&mut self, btn_layout: ButtonLayout<T>) {
|
||||
self.pad.clear();
|
||||
self.left_btn.set(btn_layout.btn_left, self.button_area);
|
||||
self.middle_btn.set(btn_layout.btn_middle, self.button_area);
|
||||
self.right_btn.set(btn_layout.btn_right, self.button_area);
|
||||
}
|
||||
|
||||
/// Setting the pressed state for all three buttons by boolean flags.
|
||||
fn set_pressed(&mut self, ctx: &mut EventCtx, left: bool, mid: bool, right: bool) {
|
||||
self.left_btn.set_pressed(ctx, left);
|
||||
self.middle_btn.set_pressed(ctx, mid);
|
||||
self.right_btn.set_pressed(ctx, right);
|
||||
}
|
||||
|
||||
/// Handle middle button hold-to-confirm start.
|
||||
/// We need to cancel possible holds in both other buttons.
|
||||
fn middle_hold_started(&mut self, ctx: &mut EventCtx) {
|
||||
self.left_btn.hold_ended(ctx);
|
||||
self.middle_btn.hold_started(ctx);
|
||||
self.right_btn.hold_ended(ctx);
|
||||
}
|
||||
|
||||
/// Handling the expiration of HTC elements.
|
||||
/// Finding out if they have been triggered and sending event
|
||||
/// for the appropriate button.
|
||||
/// Setting the state to wait for the appropriate release event
|
||||
/// from the pressed button. Also resetting visible state.
|
||||
fn handle_htc_expiration(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
event: Event,
|
||||
) -> Option<ButtonControllerMsg> {
|
||||
if self.left_btn.htc_got_triggered(ctx, event) {
|
||||
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Left);
|
||||
self.set_pressed(ctx, false, false, false);
|
||||
return Some(ButtonControllerMsg::Triggered(ButtonPos::Left));
|
||||
} else if self.middle_btn.htc_got_triggered(ctx, event) {
|
||||
// TODO: how to handle it here? Do we even need to?
|
||||
self.state = ButtonState::Nothing;
|
||||
self.set_pressed(ctx, false, false, false);
|
||||
return Some(ButtonControllerMsg::Triggered(ButtonPos::Middle));
|
||||
} else if self.right_btn.htc_got_triggered(ctx, event) {
|
||||
self.state = ButtonState::HTCNeedsRelease(PhysicalButton::Right);
|
||||
self.set_pressed(ctx, false, false, false);
|
||||
return Some(ButtonControllerMsg::Triggered(ButtonPos::Right));
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + AsRef<str>> Component for ButtonController<T> {
|
||||
type Msg = ButtonControllerMsg;
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// State machine for the ButtonController
|
||||
// We are matching event with `Event::Button` for a button action
|
||||
// and `Event::Timer` for getting the expiration of HTC.
|
||||
match event {
|
||||
Event::Button(button) => {
|
||||
let (new_state, event) = match self.state {
|
||||
ButtonState::Nothing => match button {
|
||||
ButtonEvent::ButtonPressed(which) => {
|
||||
match which {
|
||||
PhysicalButton::Left => {
|
||||
self.left_btn.hold_started(ctx);
|
||||
}
|
||||
PhysicalButton::Right => {
|
||||
self.right_btn.hold_started(ctx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
(ButtonState::OneDown(which), None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
ButtonState::OneDown(which_down) => match button {
|
||||
ButtonEvent::ButtonReleased(b) if b == which_down => match which_down {
|
||||
PhysicalButton::Left => (
|
||||
ButtonState::Nothing,
|
||||
if self.left_btn.reacts_to_single_click() {
|
||||
Some(ButtonControllerMsg::Triggered(ButtonPos::Left))
|
||||
} else {
|
||||
self.left_btn.hold_ended(ctx);
|
||||
None
|
||||
},
|
||||
),
|
||||
PhysicalButton::Right => (
|
||||
ButtonState::Nothing,
|
||||
if self.right_btn.reacts_to_single_click() {
|
||||
Some(ButtonControllerMsg::Triggered(ButtonPos::Right))
|
||||
} else {
|
||||
self.right_btn.hold_ended(ctx);
|
||||
None
|
||||
},
|
||||
),
|
||||
_ => (ButtonState::Nothing, None),
|
||||
},
|
||||
|
||||
ButtonEvent::ButtonPressed(b) if b != which_down => {
|
||||
self.middle_hold_started(ctx);
|
||||
(ButtonState::BothDown, None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
ButtonState::BothDown => match button {
|
||||
ButtonEvent::ButtonReleased(b) => {
|
||||
self.middle_btn.hold_ended(ctx);
|
||||
(ButtonState::OneReleased(b), None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
ButtonState::OneReleased(which_up) => match button {
|
||||
ButtonEvent::ButtonPressed(b) if b == which_up => {
|
||||
self.middle_hold_started(ctx);
|
||||
(ButtonState::BothDown, None)
|
||||
}
|
||||
ButtonEvent::ButtonReleased(b) if b != which_up => (
|
||||
ButtonState::Nothing,
|
||||
if self.middle_btn.reacts_to_single_click() {
|
||||
Some(ButtonControllerMsg::Triggered(ButtonPos::Middle))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
_ => (self.state, None),
|
||||
},
|
||||
ButtonState::HTCNeedsRelease(needs_release) => match button {
|
||||
// Only going out of this state if correct button was released
|
||||
ButtonEvent::ButtonReleased(released) if needs_release == released => {
|
||||
(ButtonState::Nothing, None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
};
|
||||
|
||||
// Updating the visual feedback for the buttons
|
||||
match new_state {
|
||||
// Not showing anything also when we wait for a release
|
||||
ButtonState::Nothing | ButtonState::HTCNeedsRelease(_) => {
|
||||
self.set_pressed(ctx, false, false, false);
|
||||
}
|
||||
ButtonState::OneDown(down_button) => match down_button {
|
||||
PhysicalButton::Left => {
|
||||
self.set_pressed(ctx, true, false, false);
|
||||
}
|
||||
PhysicalButton::Right => {
|
||||
self.set_pressed(ctx, false, false, true);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
ButtonState::BothDown | ButtonState::OneReleased(_) => {
|
||||
self.set_pressed(ctx, false, true, false);
|
||||
}
|
||||
};
|
||||
|
||||
self.state = new_state;
|
||||
event
|
||||
}
|
||||
Event::Timer(_) => self.handle_htc_expiration(ctx, event),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
self.left_btn.paint();
|
||||
self.middle_btn.paint();
|
||||
self.right_btn.paint();
|
||||
}
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// Saving button area so that we can re-place the buttons
|
||||
// when they get updated
|
||||
self.button_area = bounds;
|
||||
|
||||
self.pad.place(bounds);
|
||||
self.left_btn.place(bounds);
|
||||
self.middle_btn.place(bounds);
|
||||
self.right_btn.place(bounds);
|
||||
|
||||
bounds
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::ButtonContent;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for ButtonContainer<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ButtonContainer");
|
||||
|
||||
// Putting together text representation of the button
|
||||
let mut btn_text: String<30> = String::new();
|
||||
if let ButtonType::Button(btn) = &self.button_type {
|
||||
match btn.content() {
|
||||
ButtonContent::Text(text) => {
|
||||
unwrap!(btn_text.push_str(text.as_ref()));
|
||||
}
|
||||
ButtonContent::Icon(icon) => {
|
||||
unwrap!(btn_text.push_str("Icon:"));
|
||||
unwrap!(btn_text.push_str(icon.text));
|
||||
}
|
||||
}
|
||||
} else if let ButtonType::HoldToConfirm(htc) = &self.button_type {
|
||||
unwrap!(btn_text.push_str(htc.get_text().as_ref()));
|
||||
unwrap!(btn_text.push_str(" (HTC:"));
|
||||
unwrap!(btn_text.push_str(inttostr!(htc.get_duration().to_millis())));
|
||||
unwrap!(btn_text.push_str(")"));
|
||||
} else {
|
||||
unwrap!(btn_text.push_str(crate::trace::EMPTY_BTN));
|
||||
}
|
||||
t.button(btn_text.as_ref());
|
||||
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for ButtonController<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ButtonController");
|
||||
t.field("left_btn", &self.left_btn);
|
||||
t.field("middle_btn", &self.middle_btn);
|
||||
t.field("right_btn", &self.right_btn);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never, Pad},
|
||||
display::Font,
|
||||
geometry::{Point, Rect},
|
||||
};
|
||||
|
||||
use super::{common, flow_pages_poc_helpers::LineAlignment, theme};
|
||||
|
||||
/// Component that allows for "allocating" a standalone line of text anywhere
|
||||
/// on the screen and updating it arbitrarily - without affecting the rest
|
||||
/// and without being affected by other components.
|
||||
pub struct ChangingTextLine<T> {
|
||||
area: Rect,
|
||||
pad: Pad,
|
||||
text: T,
|
||||
font: Font,
|
||||
line_alignment: LineAlignment,
|
||||
}
|
||||
|
||||
impl<T> ChangingTextLine<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
pub fn new(text: T, font: Font, line_alignment: LineAlignment) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
pad: Pad::with_background(theme::BG),
|
||||
text,
|
||||
font,
|
||||
line_alignment,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_mono(text: T) -> Self {
|
||||
Self::new(text, Font::MONO, LineAlignment::Center)
|
||||
}
|
||||
|
||||
pub fn update_text(&mut self, text: T) {
|
||||
self.text = text;
|
||||
self.pad.clear();
|
||||
}
|
||||
|
||||
/// Gets the height that is needed for this line to fit perfectly
|
||||
/// without affecting the rest of the screen.
|
||||
/// (Accounting for letters that go below the baseline (y, j, ...).)
|
||||
pub fn needed_height(&self) -> i16 {
|
||||
self.font.line_height() + 2
|
||||
}
|
||||
|
||||
/// Y coordinate of text baseline, is the same for all paints.
|
||||
fn y_baseline(&self) -> i16 {
|
||||
self.area.y0 + self.font.line_height()
|
||||
}
|
||||
|
||||
fn paint_left(&self) {
|
||||
let baseline = Point::new(self.area.x0, self.y_baseline());
|
||||
common::display(baseline, &self.text, self.font)
|
||||
}
|
||||
|
||||
fn paint_center(&self) {
|
||||
let baseline = Point::new(self.area.bottom_center().x, self.y_baseline());
|
||||
common::display_center(baseline, &self.text, self.font)
|
||||
}
|
||||
|
||||
fn paint_right(&self) {
|
||||
let baseline = Point::new(self.area.x1, self.y_baseline());
|
||||
common::display_right(baseline, &self.text, self.font)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for ChangingTextLine<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.pad.place(bounds);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
match self.line_alignment {
|
||||
LineAlignment::Left => self.paint_left(),
|
||||
LineAlignment::Center => self.paint_center(),
|
||||
LineAlignment::Right => self.paint_right(),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,326 @@
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx, Pad},
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
use super::{theme, ButtonController, ButtonControllerMsg, ButtonPos, ChoiceItem, ChoiceItemAPI};
|
||||
|
||||
pub enum ChoicePageMsg {
|
||||
Choice(u8),
|
||||
LeftMost,
|
||||
RightMost,
|
||||
}
|
||||
|
||||
const MIDDLE_ROW: i32 = 72;
|
||||
|
||||
/// Interface for a specific component efficiently giving
|
||||
/// `ChoicePage` all the information it needs to render
|
||||
/// all the choice pages.
|
||||
///
|
||||
/// It avoids the need to store the whole sequence of
|
||||
/// `ChoiceItem`s in `heapless::Vec` (which caused StackOverflow),
|
||||
/// but offers a "lazy-loading" way of requesting the
|
||||
/// `ChoiceItem`s only when they are needed, one-by-one.
|
||||
/// This way, no more than one `ChoiceItem` is stored in memory at any time.
|
||||
pub trait ChoiceFactory {
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem;
|
||||
fn count(&self) -> u8;
|
||||
}
|
||||
|
||||
/// General component displaying a set of items on the screen
|
||||
/// and allowing the user to select one of them.
|
||||
///
|
||||
/// To be used by other more specific components that will
|
||||
/// supply a set of `ChoiceItem`s (through `ChoiceFactory`)
|
||||
/// and will receive back the index of the selected choice.
|
||||
///
|
||||
/// Each `ChoiceItem` is responsible for setting the screen -
|
||||
/// choosing the button text, their duration, text displayed
|
||||
/// on screen etc.
|
||||
///
|
||||
/// `is_carousel` can be used to make the choice page "infinite" -
|
||||
/// after reaching one end, users will appear at the other end.
|
||||
pub struct ChoicePage<F>
|
||||
where
|
||||
F: ChoiceFactory,
|
||||
{
|
||||
choices: F,
|
||||
pad: Pad,
|
||||
buttons: Child<ButtonController<&'static str>>,
|
||||
page_counter: u8,
|
||||
is_carousel: bool,
|
||||
}
|
||||
|
||||
impl<F> ChoicePage<F>
|
||||
where
|
||||
F: ChoiceFactory,
|
||||
{
|
||||
pub fn new(choices: F) -> Self {
|
||||
let initial_btn_layout = choices.get(0).btn_layout();
|
||||
|
||||
Self {
|
||||
choices,
|
||||
pad: Pad::with_background(theme::BG),
|
||||
buttons: Child::new(ButtonController::new(initial_btn_layout)),
|
||||
page_counter: 0,
|
||||
is_carousel: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the page counter at the very beginning.
|
||||
pub fn with_initial_page_counter(mut self, page_counter: u8) -> Self {
|
||||
self.page_counter = page_counter;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enabling the carousel mode.
|
||||
pub fn with_carousel(mut self) -> Self {
|
||||
self.is_carousel = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resetting the component, which enables reusing the same instance
|
||||
/// for multiple choice categories.
|
||||
///
|
||||
/// NOTE: from the client point of view, it would also be an option to
|
||||
/// always create a new instance with fresh setup, but I could not manage to
|
||||
/// properly clean up the previous instance - it would still be shown on
|
||||
/// screen and colliding with the new one.
|
||||
pub fn reset(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
new_choices: F,
|
||||
reset_page_counter: bool,
|
||||
is_carousel: bool,
|
||||
) {
|
||||
self.choices = new_choices;
|
||||
if reset_page_counter {
|
||||
self.page_counter = 0;
|
||||
}
|
||||
self.update(ctx);
|
||||
self.is_carousel = is_carousel;
|
||||
}
|
||||
|
||||
/// Navigating to the chosen page index.
|
||||
pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: u8) {
|
||||
self.page_counter = page_counter;
|
||||
self.update(ctx);
|
||||
}
|
||||
|
||||
/// Display current, previous and next choice according to
|
||||
/// the current ChoiceItem.
|
||||
fn paint_choices(&mut self) {
|
||||
// Performing the appropriate `paint_XXX()` for the main choice
|
||||
// and two adjacent choices when present
|
||||
// In case of carousel mode, also showing the ones from other end.
|
||||
self.show_current_choice();
|
||||
|
||||
if self.has_previous_choice() {
|
||||
self.show_previous_choice();
|
||||
} else if self.is_carousel {
|
||||
self.show_last_choice_on_left();
|
||||
}
|
||||
|
||||
if self.has_next_choice() {
|
||||
self.show_next_choice();
|
||||
} else if self.is_carousel {
|
||||
self.show_first_choice_on_right();
|
||||
}
|
||||
}
|
||||
|
||||
/// Setting current buttons, and clearing.
|
||||
fn update(&mut self, ctx: &mut EventCtx) {
|
||||
self.set_buttons(ctx);
|
||||
self.clear(ctx);
|
||||
}
|
||||
|
||||
/// Clearing the whole area and requesting repaint.
|
||||
fn clear(&mut self, ctx: &mut EventCtx) {
|
||||
self.pad.clear();
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
fn last_page_index(&self) -> u8 {
|
||||
self.choices.count() as u8 - 1
|
||||
}
|
||||
|
||||
pub fn has_previous_choice(&self) -> bool {
|
||||
self.page_counter > 0
|
||||
}
|
||||
|
||||
pub fn has_next_choice(&self) -> bool {
|
||||
self.page_counter < self.last_page_index()
|
||||
}
|
||||
|
||||
fn current_choice(&self) -> ChoiceItem {
|
||||
self.get_choice(self.page_counter)
|
||||
}
|
||||
|
||||
fn get_choice(&self, index: u8) -> ChoiceItem {
|
||||
self.choices.get(index)
|
||||
}
|
||||
|
||||
fn show_current_choice(&self) {
|
||||
self.current_choice().paint_center();
|
||||
}
|
||||
|
||||
fn show_previous_choice(&self) {
|
||||
self.get_choice(self.page_counter - 1).paint_left();
|
||||
}
|
||||
|
||||
fn show_next_choice(&self) {
|
||||
self.get_choice(self.page_counter + 1).paint_right();
|
||||
}
|
||||
|
||||
fn show_last_choice_on_left(&self) {
|
||||
self.get_choice(self.last_page_index()).paint_left();
|
||||
}
|
||||
|
||||
fn show_first_choice_on_right(&self) {
|
||||
self.get_choice(0).paint_right();
|
||||
}
|
||||
|
||||
fn decrease_page_counter(&mut self) {
|
||||
self.page_counter -= 1;
|
||||
}
|
||||
|
||||
fn increase_page_counter(&mut self) {
|
||||
self.page_counter += 1;
|
||||
}
|
||||
|
||||
fn page_counter_to_zero(&mut self) {
|
||||
self.page_counter = 0;
|
||||
}
|
||||
|
||||
fn page_counter_to_max(&mut self) {
|
||||
self.page_counter = self.last_page_index();
|
||||
}
|
||||
|
||||
pub fn page_index(&self) -> u8 {
|
||||
self.page_counter
|
||||
}
|
||||
|
||||
/// Updating the visual state of the buttons after each event.
|
||||
/// All three buttons are handled based upon the current choice.
|
||||
/// If defined in the current choice, setting their text,
|
||||
/// whether they are long-pressed, and painting them.
|
||||
///
|
||||
/// NOTE: ButtonController is handling the painting, and
|
||||
/// it will not repaint the buttons unless some of them changed.
|
||||
fn set_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
// TODO: offer the possibility to change the buttons from the client
|
||||
// (button details could be changed in the same index)
|
||||
// Use-case: BIN button in PIN is deleting last digit if the PIN is not empty,
|
||||
// otherwise causing Cancel. Would be nice to allow deleting as a single click
|
||||
// and Cancel as HTC. PIN client would check if the PIN is empty/not and
|
||||
// adjust the HTC/not.
|
||||
|
||||
let btn_layout = self.current_choice().btn_layout();
|
||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||
buttons.set(btn_layout);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Component for ChoicePage<F>
|
||||
where
|
||||
F: ChoiceFactory,
|
||||
{
|
||||
type Msg = ChoicePageMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
self.pad.place(content_area);
|
||||
self.buttons.place(button_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let button_event = self.buttons.event(ctx, event);
|
||||
|
||||
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
||||
match pos {
|
||||
ButtonPos::Left => {
|
||||
if self.has_previous_choice() {
|
||||
// Clicked BACK. Decrease the page counter.
|
||||
self.decrease_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the right end.
|
||||
self.page_counter_to_max();
|
||||
self.update(ctx);
|
||||
} else {
|
||||
// Triggered LEFTmost button. Send event
|
||||
self.clear(ctx);
|
||||
return Some(ChoicePageMsg::LeftMost);
|
||||
}
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
if self.has_next_choice() {
|
||||
// Clicked NEXT. Increase the page counter.
|
||||
self.increase_page_counter();
|
||||
self.update(ctx);
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the left end.
|
||||
self.page_counter_to_zero();
|
||||
self.update(ctx);
|
||||
} else {
|
||||
// Triggered RIGHTmost button. Send event
|
||||
self.clear(ctx);
|
||||
return Some(ChoicePageMsg::RightMost);
|
||||
}
|
||||
}
|
||||
ButtonPos::Middle => {
|
||||
// Clicked SELECT. Send current choice index
|
||||
self.clear(ctx);
|
||||
return Some(ChoicePageMsg::Choice(self.page_counter));
|
||||
}
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
self.buttons.paint();
|
||||
self.paint_choices();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F> crate::trace::Trace for ChoicePage<F>
|
||||
where
|
||||
F: ChoiceFactory,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ChoicePage");
|
||||
t.kw_pair("active_page", inttostr!(self.page_counter));
|
||||
t.kw_pair("page_count", inttostr!(self.choices.count() as u8));
|
||||
t.kw_pair("is_carousel", booltostr!(self.is_carousel));
|
||||
|
||||
if self.has_previous_choice() {
|
||||
t.field("prev_choice", &self.get_choice(self.page_counter - 1));
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the left end.
|
||||
t.field("prev_choice", &self.get_choice(self.last_page_index()));
|
||||
} else {
|
||||
t.string("prev_choice");
|
||||
t.symbol("None");
|
||||
}
|
||||
|
||||
t.field("current_choice", &self.current_choice());
|
||||
|
||||
if self.has_next_choice() {
|
||||
t.field("next_choice", &self.get_choice(self.page_counter + 1));
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the very left.
|
||||
t.field("next_choice", &self.get_choice(0));
|
||||
} else {
|
||||
t.string("next_choice");
|
||||
t.symbol("None");
|
||||
}
|
||||
|
||||
t.field("buttons", &self.buttons);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,338 @@
|
||||
use crate::ui::{geometry::Point, display::Font, util::char_to_string};
|
||||
use heapless::String;
|
||||
|
||||
use super::{
|
||||
common::{display, display_center, display_right},
|
||||
ButtonDetails, ButtonLayout,
|
||||
};
|
||||
|
||||
const MIDDLE_ROW: i16 = 61;
|
||||
const LEFT_COL: i16 = 1;
|
||||
const MIDDLE_COL: i16 = 64;
|
||||
const RIGHT_COL: i16 = 127;
|
||||
|
||||
/// Helper to unite the row height.
|
||||
fn row_height() -> i16 {
|
||||
// It never reaches the maximum height
|
||||
Font::NORMAL.line_height() - 4
|
||||
}
|
||||
|
||||
/// Component that can be used as a choice item.
|
||||
/// Allows to have a choice of anything that can be painted on screen.
|
||||
///
|
||||
/// Controls the painting of the current, previous and next item
|
||||
/// through `paint_XXX()` methods.
|
||||
/// Defines the behavior of all three buttons through `btn_XXX` attributes.
|
||||
///
|
||||
/// Possible implementations:
|
||||
/// - [x] `TextChoiceItem` - for regular text
|
||||
/// - [x] `MultilineTextChoiceItem` - for multiline text
|
||||
/// - [x] `BigCharacterChoiceItem` - for one big character
|
||||
/// - [ ] `IconChoiceItem` - for showing icons
|
||||
/// - [ ] `JustCenterChoice` - paint_left() and paint_right() show nothing
|
||||
/// - [ ] `LongStringsChoice` - paint_left() and paint_right() show ellipsis
|
||||
pub trait ChoiceItemAPI {
|
||||
fn paint_center(&mut self);
|
||||
fn paint_left(&mut self);
|
||||
fn paint_right(&mut self);
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str>;
|
||||
}
|
||||
|
||||
// TODO: consider having
|
||||
// pub trait ChoiceItemOperations {}
|
||||
|
||||
// TODO: consider storing all the text components as `T: AsRef<str>`
|
||||
// Tried, but it makes the code unnecessarily messy with all the <T>
|
||||
// definitions, which needs to be added to all the components using it.
|
||||
|
||||
/// Storing all the possible implementations of `ChoiceItemAPI`.
|
||||
/// Done like this as we want to use multiple different choice pages
|
||||
/// at the same time in `ChoicePage` - for example Multiline and BigLetters
|
||||
#[derive(Clone)]
|
||||
pub enum ChoiceItem {
|
||||
Text(TextChoiceItem),
|
||||
MultilineText(MultilineTextChoiceItem),
|
||||
BigCharacter(BigCharacterChoiceItem),
|
||||
}
|
||||
|
||||
impl ChoiceItem {
|
||||
// TODO: can we somehow avoid the repetitions here?
|
||||
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<&'static str>>) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.btn_layout.btn_left = btn_left,
|
||||
ChoiceItem::MultilineText(item) => item.btn_layout.btn_left = btn_left,
|
||||
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_left = btn_left,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_middle_btn(&mut self, btn_middle: Option<ButtonDetails<&'static str>>) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.btn_layout.btn_middle = btn_middle,
|
||||
ChoiceItem::MultilineText(item) => item.btn_layout.btn_middle = btn_middle,
|
||||
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_middle = btn_middle,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_right_btn(&mut self, btn_right: Option<ButtonDetails<&'static str>>) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.btn_layout.btn_right = btn_right,
|
||||
ChoiceItem::MultilineText(item) => item.btn_layout.btn_right = btn_right,
|
||||
ChoiceItem::BigCharacter(item) => item.btn_layout.btn_right = btn_right,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: String<50>) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.text = text,
|
||||
ChoiceItem::MultilineText(item) => item.text = text,
|
||||
ChoiceItem::BigCharacter(_) => {
|
||||
panic!("No text setting for BigCharacter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChoiceItemAPI for ChoiceItem {
|
||||
fn paint_center(&mut self) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.paint_center(),
|
||||
ChoiceItem::MultilineText(item) => item.paint_center(),
|
||||
ChoiceItem::BigCharacter(item) => item.paint_center(),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_left(&mut self) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.paint_left(),
|
||||
ChoiceItem::MultilineText(item) => item.paint_left(),
|
||||
ChoiceItem::BigCharacter(item) => item.paint_left(),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_right(&mut self) {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.paint_right(),
|
||||
ChoiceItem::MultilineText(item) => item.paint_right(),
|
||||
ChoiceItem::BigCharacter(item) => item.paint_right(),
|
||||
}
|
||||
}
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.btn_layout(),
|
||||
ChoiceItem::MultilineText(item) => item.btn_layout(),
|
||||
ChoiceItem::BigCharacter(item) => item.btn_layout(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple string component used as a choice item.
|
||||
#[derive(Clone)]
|
||||
pub struct TextChoiceItem {
|
||||
pub text: String<50>,
|
||||
pub btn_layout: ButtonLayout<&'static str>,
|
||||
}
|
||||
|
||||
impl TextChoiceItem {
|
||||
pub fn new<T>(text: T, btn_layout: ButtonLayout<&'static str>) -> Self
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
Self {
|
||||
text: String::from(text.as_ref()),
|
||||
btn_layout,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChoiceItemAPI for TextChoiceItem {
|
||||
fn paint_center(&mut self) {
|
||||
// Displaying the center choice lower than the rest,
|
||||
// to make it more clear this is the current choice
|
||||
// (and also the left and right ones do not collide with it)
|
||||
display_center(
|
||||
Point::new(MIDDLE_COL, MIDDLE_ROW + row_height()),
|
||||
&self.text,
|
||||
Font::NORMAL,
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_left(&mut self) {
|
||||
display(
|
||||
Point::new(LEFT_COL, MIDDLE_ROW),
|
||||
&self.text,
|
||||
Font::NORMAL,
|
||||
);
|
||||
}
|
||||
|
||||
fn paint_right(&mut self) {
|
||||
display_right(
|
||||
Point::new(RIGHT_COL, MIDDLE_ROW),
|
||||
&self.text,
|
||||
Font::NORMAL,
|
||||
);
|
||||
}
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
self.btn_layout.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiline string component used as a choice item.
|
||||
///
|
||||
/// Lines are delimited by '\n' character, unless specified explicitly.
|
||||
#[derive(Clone)]
|
||||
pub struct MultilineTextChoiceItem {
|
||||
// Arbitrary chosen. TODO: agree on this
|
||||
pub text: String<50>,
|
||||
delimiter: char,
|
||||
pub btn_layout: ButtonLayout<&'static str>,
|
||||
}
|
||||
|
||||
impl MultilineTextChoiceItem {
|
||||
pub fn new(text: String<50>, btn_layout: ButtonLayout<&'static str>) -> Self {
|
||||
Self {
|
||||
text,
|
||||
delimiter: '\n',
|
||||
btn_layout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows for changing the line delimiter to arbitrary char.
|
||||
pub fn use_delimiter(mut self, delimiter: char) -> Self {
|
||||
self.delimiter = delimiter;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make all the text be centered vertically - account for amount of lines.
|
||||
impl ChoiceItemAPI for MultilineTextChoiceItem {
|
||||
fn paint_center(&mut self) {
|
||||
// Displaying the center choice lower than the rest,
|
||||
// to make it more clear this is the current choice
|
||||
for (index, line) in self.text.split(self.delimiter).enumerate() {
|
||||
let offset = MIDDLE_ROW + index as i16 * row_height() + row_height();
|
||||
display_center(Point::new(MIDDLE_COL, offset), &line, Font::NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_left(&mut self) {
|
||||
for (index, line) in self.text.split(self.delimiter).enumerate() {
|
||||
let offset = MIDDLE_ROW + index as i16 * row_height();
|
||||
display(Point::new(LEFT_COL, offset), &line, Font::NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_right(&mut self) {
|
||||
for (index, line) in self.text.split(self.delimiter).enumerate() {
|
||||
let offset = MIDDLE_ROW + index as i16 * row_height();
|
||||
display_right(Point::new(RIGHT_COL, offset), &line, Font::NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
self.btn_layout.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Choice item displaying single characters in BIG font.
|
||||
/// Middle choice is magnified 4 times, left and right 2 times.
|
||||
#[derive(Clone)]
|
||||
pub struct BigCharacterChoiceItem {
|
||||
pub ch: char,
|
||||
pub btn_layout: ButtonLayout<&'static str>,
|
||||
}
|
||||
|
||||
impl BigCharacterChoiceItem {
|
||||
pub fn new(ch: char, btn_layout: ButtonLayout<&'static str>) -> Self {
|
||||
Self { ch, btn_layout }
|
||||
}
|
||||
|
||||
/// Taking the first character from the `text`.
|
||||
pub fn from_str<T>(text: T, btn_layout: ButtonLayout<&'static str>) -> Self
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
Self {
|
||||
ch: text.as_ref().chars().next().unwrap(),
|
||||
btn_layout,
|
||||
}
|
||||
}
|
||||
|
||||
fn _paint_char(&mut self, baseline: Point) {
|
||||
display(
|
||||
baseline,
|
||||
&char_to_string::<1>(self.ch),
|
||||
Font::NORMAL,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl ChoiceItemAPI for BigCharacterChoiceItem {
|
||||
fn paint_center(&mut self) {
|
||||
self._paint_char(Point::new(MIDDLE_COL - 12, MIDDLE_ROW + 9));
|
||||
}
|
||||
|
||||
fn paint_left(&mut self) {
|
||||
self._paint_char(Point::new(LEFT_COL, MIDDLE_ROW));
|
||||
}
|
||||
|
||||
fn paint_right(&mut self) {
|
||||
self._paint_char(Point::new(RIGHT_COL - 12, MIDDLE_ROW));
|
||||
}
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
self.btn_layout.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for ChoiceItem {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("ChoiceItem");
|
||||
match self {
|
||||
ChoiceItem::Text(item) => item.trace(t),
|
||||
ChoiceItem::MultilineText(item) => item.trace(t),
|
||||
ChoiceItem::BigCharacter(item) => item.trace(t),
|
||||
}
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for TextChoiceItem {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("TextChoiceItem");
|
||||
t.content_flag();
|
||||
t.string(&self.text);
|
||||
t.content_flag();
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use crate::ui::util;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for MultilineTextChoiceItem {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("MultilineTextChoiceItem");
|
||||
t.content_flag();
|
||||
t.string(&self.text);
|
||||
t.content_flag();
|
||||
t.field("delimiter", &(util::char_to_string::<1>(self.delimiter)));
|
||||
t.close();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for BigCharacterChoiceItem {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("BigCharacterChoiceItem");
|
||||
t.content_flag();
|
||||
t.string(&util::char_to_string::<1>(self.ch));
|
||||
t.content_flag();
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
use crate::ui::{
|
||||
display::{self, Font, Icon},
|
||||
geometry::{Offset, Point},
|
||||
model_tr::constant,
|
||||
};
|
||||
|
||||
use heapless::String;
|
||||
|
||||
use super::theme;
|
||||
|
||||
/// Display header text.
|
||||
pub fn display_header<T: AsRef<str>>(baseline: Point, text: T) {
|
||||
// TODO: make this centered?
|
||||
display::text(
|
||||
baseline,
|
||||
text.as_ref(),
|
||||
theme::FONT_HEADER,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
}
|
||||
|
||||
/// Display bold white text on black background
|
||||
pub fn display<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
/// Display white text on black background,
|
||||
/// centered around a baseline Point
|
||||
pub fn display_center<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_center(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
/// Display white text on black background,
|
||||
/// with right boundary at a baseline Point
|
||||
pub fn display_right<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_right(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_CHARS: usize = 18;
|
||||
const TOP_ROW_TEXT: i16 = 7;
|
||||
|
||||
/// Display indication of current user input - PIN, passphrase etc.
|
||||
/// Showing one asterisk for each character of the input.
|
||||
pub fn display_dots_center_top(dots_amount: usize, offset_from_top: i16) {
|
||||
let y_position = TOP_ROW_TEXT + offset_from_top;
|
||||
let dots_visible = dots_amount.min(MAX_VISIBLE_CHARS);
|
||||
|
||||
// String::repeat() is not available for heapless::String
|
||||
let mut dots: String<MAX_VISIBLE_CHARS> = String::new();
|
||||
for _ in 0..dots_visible {
|
||||
dots.push_str("*").unwrap();
|
||||
}
|
||||
|
||||
// Giving some notion of change even for longer-than-visible passphrases
|
||||
// - slightly shifting the dots to the left and right after each new digit
|
||||
if dots_amount > MAX_VISIBLE_CHARS && dots_amount % 2 == 0 {
|
||||
display_center(Point::new(61, y_position), &dots, Font::MONO);
|
||||
} else {
|
||||
display_center(Point::new(64, y_position), &dots, Font::MONO);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display secret input that user is currently doing - PIN, passphrase etc.
|
||||
pub fn display_secret_center_top<T: AsRef<str>>(secret: T, offset_from_top: i16) {
|
||||
let y_position = TOP_ROW_TEXT + offset_from_top;
|
||||
let char_amount = secret.as_ref().len();
|
||||
if char_amount <= MAX_VISIBLE_CHARS {
|
||||
display_center(Point::new(64, y_position), &secret, Font::MONO);
|
||||
} else {
|
||||
// Show the last part with preceding ellipsis to show something is hidden
|
||||
let ellipsis = "...";
|
||||
let offset: usize = char_amount.saturating_sub(MAX_VISIBLE_CHARS) + ellipsis.len();
|
||||
let to_show = build_string!(MAX_VISIBLE_CHARS, ellipsis, &secret.as_ref()[offset..]);
|
||||
display_center(Point::new(64, y_position), &to_show, Font::MONO);
|
||||
}
|
||||
}
|
||||
|
||||
/// Display title and possible subtitle together with a dotted line spanning
|
||||
/// the entire width.
|
||||
/// Returning the painted height of the whole header.
|
||||
pub fn paint_header<T: AsRef<str>>(top_left: Point, title: T, subtitle: Option<T>) -> i16 {
|
||||
let text_heigth = theme::FONT_HEADER.text_height();
|
||||
let title_baseline = top_left + Offset::y(text_heigth);
|
||||
display_header(title_baseline, title);
|
||||
// Optionally painting the subtitle as well
|
||||
// (and offsetting the dotted line in that case)
|
||||
let mut dotted_line_offset = text_heigth + 2;
|
||||
if let Some(subtitle) = subtitle {
|
||||
dotted_line_offset += text_heigth;
|
||||
display_header(title_baseline + Offset::y(text_heigth), subtitle);
|
||||
}
|
||||
let line_start = top_left + Offset::y(dotted_line_offset);
|
||||
display::dotted_line_horizontal(line_start, constant::WIDTH, theme::FG, 2);
|
||||
dotted_line_offset
|
||||
}
|
||||
|
||||
/// Draws icon and text on the same line - icon on the left.
|
||||
pub fn icon_with_text<T: AsRef<str>>(baseline: Point, icon: Icon, text: T, font: Font) {
|
||||
icon.draw_bottom_left(baseline, theme::FG, theme::BG);
|
||||
let text_x_offset = icon.width() + 2;
|
||||
display(baseline + Offset::x(text_x_offset), &text.as_ref(), font);
|
||||
}
|
||||
|
||||
/// Draw two lines - icon with label text (key) and another text (value) below.
|
||||
/// Returns the height painted below the given baseline.
|
||||
pub fn key_value_icon<T: AsRef<str>>(
|
||||
baseline: Point,
|
||||
icon: Icon,
|
||||
label: T,
|
||||
label_font: Font,
|
||||
value: T,
|
||||
value_font: Font,
|
||||
) -> i16 {
|
||||
icon_with_text(baseline, icon, label, label_font);
|
||||
let line_height = value_font.line_height();
|
||||
let next_line = baseline + Offset::y(line_height);
|
||||
display(next_line, &value, value_font);
|
||||
line_height
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
use crate::{
|
||||
micropython::buffer::StrBuffer,
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Pad},
|
||||
geometry::{Point, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
common, theme, ButtonAction, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos,
|
||||
FlowPages, Page,
|
||||
};
|
||||
|
||||
/// To be returned directly from Flow.
|
||||
pub enum FlowMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
// TODO: consider each FlowPage having the ability
|
||||
// to handle custom actions triggered by some btn.
|
||||
|
||||
pub struct Flow<F, const M: usize> {
|
||||
pages: FlowPages<F, M>,
|
||||
current_page: Page<M>,
|
||||
common_title: Option<StrBuffer>,
|
||||
content_area: Rect,
|
||||
pad: Pad,
|
||||
buttons: Child<ButtonController<&'static str>>,
|
||||
page_counter: u8,
|
||||
}
|
||||
|
||||
impl<F, const M: usize> Flow<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
pub fn new(pages: FlowPages<F, M>) -> Self {
|
||||
let current_page = pages.get(0);
|
||||
Self {
|
||||
pages,
|
||||
current_page,
|
||||
common_title: None,
|
||||
content_area: Rect::zero(),
|
||||
pad: Pad::with_background(theme::BG),
|
||||
// Setting empty layout for now, we do not yet know how many sub-pages the first page
|
||||
// has. Initial button layout will be set in `place()` after we can call
|
||||
// `content.page_count()`.
|
||||
buttons: Child::new(ButtonController::new(ButtonLayout::empty())),
|
||||
page_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Adding a common title to all pages. The title will not be colliding
|
||||
/// with the page content, as the content will be offset.
|
||||
pub fn with_common_title(mut self, title: StrBuffer) -> Self {
|
||||
self.common_title = Some(title);
|
||||
self
|
||||
}
|
||||
|
||||
/// Placing current page, setting current buttons and clearing.
|
||||
fn update(&mut self, ctx: &mut EventCtx, get_new_page: bool) {
|
||||
if get_new_page {
|
||||
self.current_page = self.pages.get(self.page_counter);
|
||||
}
|
||||
let content_area = self.content_area;
|
||||
self.current_page.place(content_area);
|
||||
self.set_buttons(ctx);
|
||||
self.clear(ctx);
|
||||
}
|
||||
|
||||
/// Clearing the whole area and requesting repaint.
|
||||
fn clear(&mut self, ctx: &mut EventCtx) {
|
||||
self.pad.clear();
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Going to the previous page.
|
||||
fn go_to_prev_page(&mut self, ctx: &mut EventCtx) {
|
||||
self.page_counter -= 1;
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Going to the next page.
|
||||
fn go_to_next_page(&mut self, ctx: &mut EventCtx) {
|
||||
self.page_counter += 1;
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Going to page by its absolute index.
|
||||
/// Negative index means counting from the end.
|
||||
fn go_to_page_absolute(&mut self, index: i16, ctx: &mut EventCtx) {
|
||||
if index < 0 {
|
||||
self.page_counter = (self.pages.count() as i16 + index) as u8;
|
||||
} else {
|
||||
self.page_counter = index as u8;
|
||||
}
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Jumping to another page relative to the current one.
|
||||
fn go_to_page_relative(&mut self, jump: i16, ctx: &mut EventCtx) {
|
||||
self.page_counter = (self.page_counter as i16 + jump) as u8;
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Updating the visual state of the buttons after each event.
|
||||
/// All three buttons are handled based upon the current choice.
|
||||
/// If defined in the current choice, setting their text,
|
||||
/// whether they are long-pressed, and painting them.
|
||||
///
|
||||
/// NOTE: ButtonController is handling the painting, and
|
||||
/// it will not repaint the buttons unless some of them changed.
|
||||
fn set_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
let btn_layout = self.current_page.btn_layout();
|
||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||
buttons.set(btn_layout);
|
||||
});
|
||||
}
|
||||
|
||||
/// When current choice contains paginated content, it may use the button
|
||||
/// event to just paginate itself.
|
||||
fn event_consumed_by_current_choice(&mut self, ctx: &mut EventCtx, pos: ButtonPos) -> bool {
|
||||
if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() {
|
||||
self.current_page.go_to_prev_page();
|
||||
self.update(ctx, false);
|
||||
true
|
||||
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
|
||||
self.current_page.go_to_next_page();
|
||||
self.update(ctx, false);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, const M: usize> Component for Flow<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
type Msg = FlowMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
// Accounting for possible title
|
||||
let content_area = if self.common_title.is_some() {
|
||||
title_content_area.split_top(10).1
|
||||
} else {
|
||||
title_content_area
|
||||
};
|
||||
self.content_area = content_area;
|
||||
|
||||
// We finally found how long is the first page, and can set its button layout.
|
||||
self.current_page.place(content_area);
|
||||
self.buttons = Child::new(ButtonController::new(self.current_page.btn_layout()));
|
||||
|
||||
self.pad.place(title_content_area);
|
||||
self.buttons.place(button_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let button_event = self.buttons.event(ctx, event);
|
||||
|
||||
// Do something when a button was triggered
|
||||
// and we have some action connected with it
|
||||
if let Some(ButtonControllerMsg::Triggered(pos)) = button_event {
|
||||
// When there is a previous or next screen in the current flow,
|
||||
// handle that first and in case it triggers, then do not continue
|
||||
if self.event_consumed_by_current_choice(ctx, pos) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let actions = self.current_page.btn_actions();
|
||||
let action = actions.get_action(pos);
|
||||
if let Some(action) = action {
|
||||
match action {
|
||||
ButtonAction::PrevPage => {
|
||||
self.go_to_prev_page(ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::NextPage => {
|
||||
self.go_to_next_page(ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::GoToIndex(index) => {
|
||||
self.go_to_page_absolute(index, ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::MovePageRelative(jump) => {
|
||||
self.go_to_page_relative(jump, ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::Cancel => return Some(FlowMsg::Cancelled),
|
||||
ButtonAction::Confirm => return Some(FlowMsg::Confirmed),
|
||||
ButtonAction::Select => {}
|
||||
ButtonAction::Action(_) => {}
|
||||
}
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// TODO: might put horizontal scrollbar at the top right
|
||||
self.pad.paint();
|
||||
self.buttons.paint();
|
||||
if let Some(title) = &self.common_title {
|
||||
common::paint_header(Point::zero(), title, None);
|
||||
}
|
||||
self.current_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use heapless::String;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F, const M: usize> crate::trace::Trace for Flow<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
/// Accounting for the possibility that button is connected with the
|
||||
/// currently paginated flow_page (only Prev or Next in that case).
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
if matches!(pos, ButtonPos::Left) && self.current_page.has_prev_page() {
|
||||
ButtonAction::PrevPage.string()
|
||||
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
|
||||
ButtonAction::NextPage.string()
|
||||
} else {
|
||||
let btn_actions = self.current_page.btn_actions();
|
||||
|
||||
match btn_actions.get_action(pos) {
|
||||
Some(action) => action.string(),
|
||||
None => ButtonAction::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Flow");
|
||||
t.kw_pair("flow_page", inttostr!(self.page_counter));
|
||||
t.kw_pair("flow_page_count", inttostr!(self.pages.count()));
|
||||
|
||||
self.report_btn_actions(t);
|
||||
|
||||
if let Some(title) = &self.common_title {
|
||||
t.title(title.as_ref());
|
||||
}
|
||||
t.field("content_area", &self.content_area);
|
||||
t.field("buttons", &self.buttons);
|
||||
t.field("flow_page", &self.current_page);
|
||||
t.close()
|
||||
}
|
||||
}
|
@ -0,0 +1,309 @@
|
||||
use crate::{
|
||||
micropython::{buffer::StrBuffer},
|
||||
ui::{
|
||||
component::Paginate,
|
||||
display::{Font, Icon, IconAndName},
|
||||
geometry::{Offset, Rect},
|
||||
model_tr::theme,
|
||||
util::ResultExt
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
use super::{
|
||||
flow_pages_poc_helpers::{
|
||||
LayoutFit, LayoutSink, LineAlignment, Op, TextLayout, TextNoOp, TextRenderer, TextStyle,
|
||||
ToDisplay,
|
||||
},
|
||||
ButtonActions, ButtonDetails, ButtonLayout,
|
||||
};
|
||||
|
||||
/// Holding specific workflows that are created in `layout.rs`.
|
||||
/// Is returning a `Page` (page/screen) on demand
|
||||
/// based on the current page in `Flow`.
|
||||
/// Before, when `layout.rs` was defining a `heapless::Vec` of `Page`s,
|
||||
/// it was a very stack-expensive operation and StackOverflow was encountered.
|
||||
/// With this "lazy-loading" approach (creating each page on demand) we can
|
||||
/// have theoretically unlimited number of pages without triggering SO.
|
||||
/// (Currently only the current page is stored on stack - in
|
||||
/// `Flow::current_page`.)
|
||||
pub struct FlowPages<F, const M: usize> {
|
||||
/// Function/closure that will return appropriate page on demand.
|
||||
get_page: F,
|
||||
/// Number of pages in the flow.
|
||||
page_count: u8,
|
||||
}
|
||||
|
||||
impl<F, const M: usize> FlowPages<F, M>
|
||||
where
|
||||
F: Fn(u8) -> Page<M>,
|
||||
{
|
||||
pub fn new(get_page: F, page_count: u8) -> Self {
|
||||
Self {
|
||||
get_page,
|
||||
page_count,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, page_index: u8) -> Page<M> {
|
||||
(self.get_page)(page_index)
|
||||
}
|
||||
|
||||
pub fn count(&self) -> u8 {
|
||||
self.page_count
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Page<const M: usize> {
|
||||
ops: Vec<Op, M>,
|
||||
layout: TextLayout,
|
||||
btn_layout: ButtonLayout<&'static str>,
|
||||
btn_actions: ButtonActions,
|
||||
current_page: usize,
|
||||
page_count: usize,
|
||||
char_offset: usize,
|
||||
}
|
||||
|
||||
// For `layout.rs`
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn new(btn_layout: ButtonLayout<&'static str>, btn_actions: ButtonActions) -> Self {
|
||||
let style = TextStyle::new(
|
||||
Font::NORMAL,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
theme::FG,
|
||||
theme::FG,
|
||||
);
|
||||
Self {
|
||||
ops: Vec::new(),
|
||||
layout: TextLayout::new(style),
|
||||
btn_layout,
|
||||
btn_actions,
|
||||
current_page: 0,
|
||||
page_count: 1,
|
||||
char_offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For `flow.rs`
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn paint(&mut self) {
|
||||
self.change_page(self.current_page);
|
||||
self.layout_content(&mut TextRenderer);
|
||||
}
|
||||
|
||||
pub fn btn_layout(&self) -> ButtonLayout<&'static str> {
|
||||
// When we are in pagination inside this flow,
|
||||
// show the up and down arrows on appropriate sides
|
||||
let current = self.btn_layout.clone();
|
||||
|
||||
let btn_left = if self.has_prev_page() {
|
||||
Some(ButtonDetails::up_arrow_icon_wide())
|
||||
} else {
|
||||
current.btn_left
|
||||
};
|
||||
let btn_right = if self.has_next_page() {
|
||||
Some(ButtonDetails::down_arrow_icon_wide())
|
||||
} else {
|
||||
current.btn_right
|
||||
};
|
||||
|
||||
ButtonLayout::new(btn_left, current.btn_middle, btn_right)
|
||||
}
|
||||
|
||||
pub fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.layout.bounds = bounds;
|
||||
self.page_count = self.page_count();
|
||||
bounds
|
||||
}
|
||||
|
||||
pub fn btn_actions(&self) -> ButtonActions {
|
||||
self.btn_actions.clone()
|
||||
}
|
||||
|
||||
pub fn has_prev_page(&self) -> bool {
|
||||
self.current_page > 0
|
||||
}
|
||||
|
||||
pub fn has_next_page(&self) -> bool {
|
||||
self.current_page < self.page_count - 1
|
||||
}
|
||||
|
||||
pub fn go_to_prev_page(&mut self) {
|
||||
self.current_page -= 1;
|
||||
}
|
||||
|
||||
pub fn go_to_next_page(&mut self) {
|
||||
self.current_page += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// For `layout.rs` - single operations
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn with_new_item(mut self, item: Op) -> Self {
|
||||
self.ops
|
||||
.push(item)
|
||||
.assert_if_debugging_ui("Could not push to self.ops");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text(self, text: StrBuffer) -> Self {
|
||||
self.with_new_item(Op::Text(ToDisplay::new(text)))
|
||||
}
|
||||
|
||||
pub fn newline(self) -> Self {
|
||||
self.with_new_item(Op::Text(ToDisplay::new("\n".into())))
|
||||
}
|
||||
|
||||
pub fn newline_half(self) -> Self {
|
||||
self.with_new_item(Op::Text(ToDisplay::new("\r".into())))
|
||||
}
|
||||
|
||||
pub fn next_page(self) -> Self {
|
||||
self.with_new_item(Op::NextPage)
|
||||
}
|
||||
|
||||
pub fn icon(self, icon: IconAndName) -> Self {
|
||||
self.with_new_item(Op::Icon(Icon::new(icon)))
|
||||
}
|
||||
|
||||
pub fn font(self, font: Font) -> Self {
|
||||
self.with_new_item(Op::Font(font))
|
||||
}
|
||||
|
||||
pub fn offset(self, offset: Offset) -> Self {
|
||||
self.with_new_item(Op::CursorOffset(offset))
|
||||
}
|
||||
|
||||
pub fn alignment(self, alignment: LineAlignment) -> Self {
|
||||
self.with_new_item(Op::LineAlignment(alignment))
|
||||
}
|
||||
}
|
||||
|
||||
// For `layout.rs` - aggregating operations
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn icon_label_text(self, icon: IconAndName, label: StrBuffer, text: StrBuffer) -> Self {
|
||||
self.icon_with_offset(icon, 3)
|
||||
.text_normal(label)
|
||||
.newline()
|
||||
.text_bold(text)
|
||||
}
|
||||
|
||||
pub fn icon_with_offset(self, icon: IconAndName, x_offset: i16) -> Self {
|
||||
self.icon(icon).offset(Offset::x(x_offset))
|
||||
}
|
||||
|
||||
pub fn text_normal(self, text: StrBuffer) -> Self {
|
||||
self.font(Font::NORMAL).text(text)
|
||||
}
|
||||
|
||||
pub fn text_bold(self, text: StrBuffer) -> Self {
|
||||
self.font(Font::BOLD).text(text)
|
||||
}
|
||||
}
|
||||
|
||||
// For painting and pagination
|
||||
impl<const M: usize> Page<M> {
|
||||
pub fn set_char_offset(&mut self, char_offset: usize) {
|
||||
self.char_offset = char_offset;
|
||||
}
|
||||
|
||||
pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit {
|
||||
let mut cursor = self.layout.initial_cursor();
|
||||
self.layout
|
||||
.layout_ops(self.ops.clone(), &mut cursor, self.char_offset, sink)
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
impl<const M: usize> Paginate for Page<M> {
|
||||
fn page_count(&mut self) -> usize {
|
||||
let mut page_count = 1; // There's always at least one page.
|
||||
let mut char_offset = 0;
|
||||
|
||||
// Make sure we're starting from the beginning.
|
||||
self.set_char_offset(char_offset);
|
||||
|
||||
// Looping through the content and counting pages
|
||||
// until we finally fit.
|
||||
loop {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
LayoutFit::Fitting { .. } => {
|
||||
break; // TODO: We should consider if there's more content
|
||||
// to render.
|
||||
}
|
||||
LayoutFit::OutOfBounds {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
page_count += 1;
|
||||
char_offset += processed_chars;
|
||||
self.set_char_offset(char_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the char offset back to the beginning.
|
||||
self.set_char_offset(0);
|
||||
|
||||
page_count
|
||||
}
|
||||
|
||||
fn change_page(&mut self, to_page: usize) {
|
||||
let mut active_page = 0;
|
||||
let mut char_offset = 0;
|
||||
|
||||
// Make sure we're starting from the beginning.
|
||||
self.set_char_offset(char_offset);
|
||||
|
||||
// Looping through the content until we arrive at
|
||||
// the wanted page.
|
||||
while active_page < to_page {
|
||||
let fit = self.layout_content(&mut TextNoOp);
|
||||
match fit {
|
||||
LayoutFit::Fitting { .. } => {
|
||||
break; // TODO: We should consider if there's more content
|
||||
// to render.
|
||||
}
|
||||
LayoutFit::OutOfBounds {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
active_page += 1;
|
||||
char_offset += processed_chars;
|
||||
self.set_char_offset(char_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
pub mod trace {
|
||||
use crate::ui::model_tr::component::flow_pages_poc_helpers::TraceSink;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub struct TraceText<'a, const M: usize>(pub &'a Page<M>);
|
||||
|
||||
impl<'a, const M: usize> crate::trace::Trace for TraceText<'a, M> {
|
||||
fn trace(&self, d: &mut dyn crate::trace::Tracer) {
|
||||
d.content_flag();
|
||||
self.0.layout_content(&mut TraceSink(d));
|
||||
d.content_flag();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<const M: usize> crate::trace::Trace for Page<M> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("Page");
|
||||
t.kw_pair("active_page", inttostr!(self.current_page as u8));
|
||||
t.kw_pair("page_count", inttostr!(self.page_count as u8));
|
||||
t.field("content", &trace::TraceText(self));
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,716 @@
|
||||
//! Mostly copy-pasted stuff from ui/component/text,
|
||||
//! but with small modifications.
|
||||
//! It is really mostly changing Op::Text(&'a str) to Op::Text(String<100>),
|
||||
//! having self.ops as Vec<Op, 30> and changes revolving around it.
|
||||
//! Even if some stuff could be reused now, I copy-pasted it anyway, as this
|
||||
//! extension for Icons, Offsets, etc. should no longer live in
|
||||
//! ui/component/text, and so they can be freely removed (as they are here as
|
||||
//! well).
|
||||
|
||||
use crate::{
|
||||
micropython::buffer::StrBuffer,
|
||||
ui::{
|
||||
display::{self, Color, Font, Icon},
|
||||
geometry::{Offset, Point, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
// TODO: consider moving into T: AsRef<str> instead if StrBuffer?
|
||||
#[derive(Clone)]
|
||||
pub struct ToDisplay {
|
||||
pub text: StrBuffer,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
impl ToDisplay {
|
||||
pub fn new(text: StrBuffer) -> Self {
|
||||
Self {
|
||||
text: text.clone(),
|
||||
length: text.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Operations that can be done on FormattedText.
|
||||
#[derive(Clone)]
|
||||
pub enum Op {
|
||||
/// Render text with current color and font.
|
||||
Text(ToDisplay),
|
||||
/// Render icon.
|
||||
Icon(Icon),
|
||||
/// Set current text color.
|
||||
Color(Color),
|
||||
/// Set currently used font.
|
||||
Font(Font),
|
||||
/// Set currently used line alignment.
|
||||
LineAlignment(LineAlignment),
|
||||
/// Move the current cursor by specified Offset.
|
||||
CursorOffset(Offset),
|
||||
/// Force continuing on the next page.
|
||||
NextPage,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum LineBreaking {
|
||||
/// Break line only at whitespace, if possible. If we don't find any
|
||||
/// whitespace, break words.
|
||||
BreakAtWhitespace,
|
||||
/// Break words, adding a hyphen before the line-break. Does not use any
|
||||
/// smart algorithm, just char-by-char.
|
||||
BreakWordsAndInsertHyphen,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum PageBreaking {
|
||||
/// Stop after hitting the bottom-right edge of the bounds.
|
||||
Cut,
|
||||
/// Before stopping at the bottom-right edge, insert ellipsis to signify
|
||||
/// more content is available, but only if no hyphen has been inserted yet.
|
||||
CutAndInsertEllipsis,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum LineAlignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Visual instructions for laying out a formatted block of text.
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TextLayout {
|
||||
/// Bounding box restricting the layout dimensions.
|
||||
pub bounds: Rect,
|
||||
|
||||
/// Additional space before beginning of text, can be negative to shift text
|
||||
/// upwards.
|
||||
pub padding_top: i16,
|
||||
/// Additional space between end of text and bottom of bounding box, can be
|
||||
/// negative.
|
||||
pub padding_bottom: i16,
|
||||
|
||||
/// Fonts, colors, line/page breaking behavior.
|
||||
pub style: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TextStyle {
|
||||
/// Text font ID. Can be overridden by `Op::Font`.
|
||||
pub text_font: Font,
|
||||
/// Text color. Can be overridden by `Op::Color`.
|
||||
pub text_color: Color,
|
||||
/// Background color.
|
||||
pub background_color: Color,
|
||||
|
||||
/// Foreground color used for drawing the hyphen.
|
||||
pub hyphen_color: Color,
|
||||
/// Foreground color used for drawing the ellipsis.
|
||||
pub ellipsis_color: Color,
|
||||
|
||||
/// Specifies which line-breaking strategy to use.
|
||||
pub line_breaking: LineBreaking,
|
||||
/// Specifies what to do at the end of the page.
|
||||
pub page_breaking: PageBreaking,
|
||||
|
||||
/// Specifies how to align text on the line.
|
||||
pub line_alignment: LineAlignment,
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
pub const fn new(
|
||||
text_font: Font,
|
||||
text_color: Color,
|
||||
background_color: Color,
|
||||
hyphen_color: Color,
|
||||
ellipsis_color: Color,
|
||||
) -> Self {
|
||||
TextStyle {
|
||||
text_font,
|
||||
text_color,
|
||||
background_color,
|
||||
hyphen_color,
|
||||
ellipsis_color,
|
||||
line_breaking: LineBreaking::BreakAtWhitespace,
|
||||
page_breaking: PageBreaking::CutAndInsertEllipsis,
|
||||
line_alignment: LineAlignment::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextLayout {
|
||||
/// Create a new text layout, with empty size and default text parameters
|
||||
/// filled from `T`.
|
||||
pub fn new(style: TextStyle) -> Self {
|
||||
Self {
|
||||
bounds: Rect::zero(),
|
||||
padding_top: 0,
|
||||
padding_bottom: 0,
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_bounds(mut self, bounds: Rect) -> Self {
|
||||
self.bounds = bounds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Baseline `Point` where we are starting to draw the text.
|
||||
pub fn initial_cursor(&self) -> Point {
|
||||
self.bounds.top_left() + Offset::y(self.style.text_font.text_height() + self.padding_top)
|
||||
}
|
||||
|
||||
/// Trying to fit the content on the current screen.
|
||||
pub fn fit_text(&self, text: &str) -> LayoutFit {
|
||||
self.layout_text(text, &mut self.initial_cursor(), &mut TextNoOp)
|
||||
}
|
||||
|
||||
/// Draw as much text as possible on the current screen.
|
||||
pub fn render_text(&self, text: &str) {
|
||||
self.layout_text(text, &mut self.initial_cursor(), &mut TextRenderer);
|
||||
}
|
||||
|
||||
/// Y coordinate of the bottom of the available space/bounds
|
||||
pub fn bottom_y(&self) -> i16 {
|
||||
(self.bounds.y1 - self.padding_bottom).max(self.bounds.y0)
|
||||
}
|
||||
|
||||
/// X coordinate of the right of the available space/bounds
|
||||
pub fn right_x(&self) -> i16 {
|
||||
self.bounds.x1
|
||||
}
|
||||
|
||||
/// Perform some operations defined on `Op` for a list of those `Op`s
|
||||
/// - e.g. changing the color, changing the font or rendering the text.
|
||||
pub fn layout_ops<const M: usize>(
|
||||
mut self,
|
||||
ops: Vec<Op, M>,
|
||||
cursor: &mut Point,
|
||||
skip_bytes: usize,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit {
|
||||
let init_cursor = *cursor;
|
||||
let mut total_processed_chars = 0;
|
||||
|
||||
let mut skipped = 0;
|
||||
for op in ops {
|
||||
let real_op = {
|
||||
match op {
|
||||
Op::Text(to_display) if skipped < skip_bytes => {
|
||||
skipped = skipped.saturating_add(to_display.length);
|
||||
if skipped > skip_bytes {
|
||||
let leave_bytes = skipped - skip_bytes;
|
||||
let new_display = ToDisplay {
|
||||
text: to_display.text,
|
||||
length: leave_bytes,
|
||||
};
|
||||
Some(Op::Text(new_display))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Op::Icon(_) if skipped < skip_bytes => {
|
||||
// Assume the icon accounts for one character
|
||||
skipped = skipped.saturating_add(1);
|
||||
None
|
||||
}
|
||||
Op::NextPage if skipped < skip_bytes => {
|
||||
// Skip the next page and consider it one character
|
||||
skipped = skipped.saturating_add(1);
|
||||
None
|
||||
}
|
||||
Op::CursorOffset(_) if skipped < skip_bytes => {
|
||||
// Skip any offsets
|
||||
None
|
||||
}
|
||||
op_to_pass_through => Some(op_to_pass_through.clone()),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(op) = real_op {
|
||||
match op {
|
||||
// Changing color
|
||||
Op::Color(color) => {
|
||||
self.style.text_color = color;
|
||||
}
|
||||
// Changing font
|
||||
Op::Font(font) => {
|
||||
self.style.text_font = font;
|
||||
}
|
||||
// Changing line/text alignment
|
||||
Op::LineAlignment(line_alignment) => {
|
||||
self.style.line_alignment = line_alignment;
|
||||
}
|
||||
// Moving the cursor
|
||||
Op::CursorOffset(offset) => {
|
||||
cursor.x += offset.x;
|
||||
cursor.y += offset.y;
|
||||
}
|
||||
// Moving to the next page
|
||||
Op::NextPage => {
|
||||
// Pretending that nothing more fits on current page to force
|
||||
// continuing on the next one
|
||||
// Making that to account for one character for pagination purposes
|
||||
total_processed_chars += 1;
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: total_processed_chars,
|
||||
height: self.layout_height(init_cursor, *cursor),
|
||||
};
|
||||
}
|
||||
// Drawing text or icon
|
||||
Op::Text(_) | Op::Icon(_) => {
|
||||
// Text and Icon behave similarly - we try to fit them
|
||||
// on the current page and if they do not fit,
|
||||
// return the appropriate OutOfBounds message
|
||||
let fit = if let Op::Text(to_display) = op {
|
||||
let text = to_display.text.as_ref();
|
||||
let text_len = to_display.length;
|
||||
let start = text.len() - text_len;
|
||||
let to_really_display = &text[start..];
|
||||
// let to_really_display = text.text[text.start..text.end].to_string();
|
||||
self.layout_text(to_really_display, cursor, sink)
|
||||
} else if let Op::Icon(icon) = op {
|
||||
self.layout_icon(icon, cursor, sink)
|
||||
} else {
|
||||
panic!("unexpected op type");
|
||||
};
|
||||
|
||||
match fit {
|
||||
LayoutFit::Fitting {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
total_processed_chars += processed_chars;
|
||||
}
|
||||
LayoutFit::OutOfBounds {
|
||||
processed_chars, ..
|
||||
} => {
|
||||
total_processed_chars += processed_chars;
|
||||
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: total_processed_chars,
|
||||
height: self.layout_height(init_cursor, *cursor),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutFit::Fitting {
|
||||
processed_chars: total_processed_chars,
|
||||
height: self.layout_height(init_cursor, *cursor),
|
||||
}
|
||||
}
|
||||
|
||||
/// Loop through the `text` and try to fit it on the current screen,
|
||||
/// reporting events to `sink`, which may do something with them (e.g. draw
|
||||
/// on screen).
|
||||
pub fn layout_text(
|
||||
&self,
|
||||
text: &str,
|
||||
cursor: &mut Point,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit {
|
||||
let init_cursor = *cursor;
|
||||
let mut remaining_text = text;
|
||||
|
||||
// Check if bounding box is high enough for at least one line.
|
||||
if cursor.y > self.bottom_y() {
|
||||
sink.out_of_bounds();
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
|
||||
while !remaining_text.is_empty() {
|
||||
let span = Span::fit_horizontally(
|
||||
remaining_text,
|
||||
self.bounds.x1 - cursor.x,
|
||||
self.style.text_font,
|
||||
self.style.line_breaking,
|
||||
);
|
||||
|
||||
// Report the span at the cursor position.
|
||||
// Not doing it when the span length is 0, as that
|
||||
// means we encountered a newline/line-break, which we do not draw.
|
||||
// Line-breaks are reported later.
|
||||
if span.length > 0 {
|
||||
sink.text(*cursor, self, &remaining_text[..span.length]);
|
||||
}
|
||||
|
||||
// Continue with the rest of the remaining_text.
|
||||
remaining_text = &remaining_text[span.length + span.skip_next_chars..];
|
||||
|
||||
// Advance the cursor horizontally.
|
||||
cursor.x += span.advance.x;
|
||||
|
||||
if span.advance.y > 0 {
|
||||
// We're advancing to the next line.
|
||||
|
||||
// Check if we should be appending a hyphen at this point.
|
||||
if span.insert_hyphen_before_line_break {
|
||||
sink.hyphen(*cursor, self);
|
||||
}
|
||||
// Check the amount of vertical space we have left.
|
||||
if cursor.y + span.advance.y > self.bottom_y() {
|
||||
if !remaining_text.is_empty() {
|
||||
// Append ellipsis to indicate more content is available, but only if we
|
||||
// haven't already appended a hyphen.
|
||||
let should_append_ellipsis =
|
||||
matches!(self.style.page_breaking, PageBreaking::CutAndInsertEllipsis)
|
||||
&& !span.insert_hyphen_before_line_break;
|
||||
if should_append_ellipsis {
|
||||
sink.ellipsis(*cursor, self);
|
||||
}
|
||||
// TODO: This does not work in case we are the last
|
||||
// fitting text token on the line, with more text tokens
|
||||
// following and `text.is_empty() == true`.
|
||||
}
|
||||
|
||||
// Report we are out of bounds and quit.
|
||||
sink.out_of_bounds();
|
||||
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: text.len() - remaining_text.len(),
|
||||
height: self.layout_height(init_cursor, *cursor),
|
||||
};
|
||||
} else {
|
||||
// Advance the cursor to the beginning of the next line.
|
||||
cursor.x = self.bounds.x0;
|
||||
cursor.y += span.advance.y;
|
||||
|
||||
// Report a line break. While rendering works using the cursor coordinates, we
|
||||
// use explicit line-break reporting in the `Trace` impl.
|
||||
sink.line_break(*cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutFit::Fitting {
|
||||
processed_chars: text.len(),
|
||||
height: self.layout_height(init_cursor, *cursor),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to fit the `icon` on the current screen
|
||||
pub fn layout_icon(
|
||||
&self,
|
||||
icon: Icon,
|
||||
cursor: &mut Point,
|
||||
sink: &mut dyn LayoutSink,
|
||||
) -> LayoutFit {
|
||||
// Check if bounding box is high enough for at least one line.
|
||||
if cursor.y > self.bottom_y() {
|
||||
sink.out_of_bounds();
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: 0,
|
||||
height: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Icon is too wide to fit on current line.
|
||||
// Trying to accommodate it on the next line, when it exists on this page.
|
||||
if cursor.x + icon.width() > self.right_x() {
|
||||
cursor.x = self.bounds.x0;
|
||||
cursor.y += self.style.text_font.line_height();
|
||||
if cursor.y > self.bottom_y() {
|
||||
sink.out_of_bounds();
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: 0,
|
||||
height: self.style.text_font.line_height(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
sink.icon(*cursor, self, icon);
|
||||
|
||||
// TODO: currently we are using just small icons - that fit nicely to one line -
|
||||
// but in case we would do bigger ones, we would need some anti-collision
|
||||
// mechanism.
|
||||
|
||||
cursor.x += icon.width() as i16;
|
||||
LayoutFit::Fitting {
|
||||
// TODO: how to handle this? It could collide with "skip_first_n_bytes"
|
||||
processed_chars: 1,
|
||||
height: 0, // it should just draw on one line
|
||||
}
|
||||
}
|
||||
|
||||
/// Overall height of the content, including paddings.
|
||||
fn layout_height(&self, init_cursor: Point, end_cursor: Point) -> i16 {
|
||||
self.padding_top
|
||||
+ self.style.text_font.text_height()
|
||||
+ (end_cursor.y - init_cursor.y)
|
||||
+ self.padding_bottom
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether we can fit content on the current screen.
|
||||
/// Knows how many characters got processed and how high the content is.
|
||||
pub enum LayoutFit {
|
||||
/// Entire content fits. Vertical size is returned in `height`.
|
||||
Fitting { processed_chars: usize, height: i16 },
|
||||
/// Content fits partially or not at all.
|
||||
OutOfBounds { processed_chars: usize, height: i16 },
|
||||
}
|
||||
|
||||
impl LayoutFit {
|
||||
/// How high is the processed/fitted content.
|
||||
pub fn height(&self) -> i16 {
|
||||
match self {
|
||||
LayoutFit::Fitting { height, .. } => *height,
|
||||
LayoutFit::OutOfBounds { height, .. } => *height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: LayoutSink could support even things like drawing icons
|
||||
// or making custom x or y offsets from any position
|
||||
|
||||
/// Visitor for text segment operations.
|
||||
/// Defines responses for certain kind of events encountered
|
||||
/// when processing the content.
|
||||
pub trait LayoutSink {
|
||||
/// Text should be processed.
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, _text: &str) {}
|
||||
/// Text should be processed.
|
||||
fn icon(&mut self, _cursor: Point, _layout: &TextLayout, _icon: Icon) {}
|
||||
/// Hyphen at the end of line.
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
/// Ellipsis at the end of the page.
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {}
|
||||
/// Line break - a newline.
|
||||
fn line_break(&mut self, _cursor: Point) {}
|
||||
/// Content cannot fit on the screen.
|
||||
fn out_of_bounds(&mut self) {}
|
||||
}
|
||||
|
||||
/// `LayoutSink` without any functionality.
|
||||
/// Used to consume events when counting pages
|
||||
/// or navigating to a certain page number.
|
||||
pub struct TextNoOp;
|
||||
|
||||
impl LayoutSink for TextNoOp {}
|
||||
|
||||
/// `LayoutSink` for rendering the content.
|
||||
pub struct TextRenderer;
|
||||
|
||||
impl LayoutSink for TextRenderer {
|
||||
fn text(&mut self, cursor: Point, layout: &TextLayout, text: &str) {
|
||||
// Accounting for the line-alignment - left, right or center.
|
||||
// Assume the current line can be drawn on from the cursor
|
||||
// to the right side of the screen.
|
||||
|
||||
match layout.style.line_alignment {
|
||||
LineAlignment::Left => {
|
||||
display::text(
|
||||
cursor,
|
||||
text,
|
||||
layout.style.text_font,
|
||||
layout.style.text_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
}
|
||||
LineAlignment::Center => {
|
||||
let center = Point::new(cursor.x + (layout.bounds.x1 - cursor.x) / 2, cursor.y);
|
||||
display::text_center(
|
||||
center,
|
||||
text,
|
||||
layout.style.text_font,
|
||||
layout.style.text_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
}
|
||||
LineAlignment::Right => {
|
||||
let right = Point::new(layout.bounds.x1, cursor.y);
|
||||
display::text_right(
|
||||
right,
|
||||
text,
|
||||
layout.style.text_font,
|
||||
layout.style.text_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hyphen(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
display::text(
|
||||
cursor,
|
||||
"-",
|
||||
layout.style.text_font,
|
||||
layout.style.hyphen_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, cursor: Point, layout: &TextLayout) {
|
||||
display::text(
|
||||
cursor,
|
||||
"...",
|
||||
layout.style.text_font,
|
||||
layout.style.ellipsis_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
fn icon(&mut self, cursor: Point, layout: &TextLayout, icon: Icon) {
|
||||
icon.draw_bottom_left(
|
||||
cursor,
|
||||
layout.style.text_color,
|
||||
layout.style.background_color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `LayoutSink` for debugging purposes.
|
||||
#[cfg(feature = "ui_debug")]
|
||||
pub struct TraceSink<'a>(pub &'a mut dyn crate::trace::Tracer);
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use crate::trace::Trace;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<'a> LayoutSink for TraceSink<'a> {
|
||||
fn text(&mut self, _cursor: Point, _layout: &TextLayout, text: &str) {
|
||||
self.0.string(text);
|
||||
}
|
||||
|
||||
fn icon(&mut self, _cursor: Point, _layout: &TextLayout, icon: Icon) {
|
||||
icon.trace(self.0);
|
||||
}
|
||||
|
||||
fn hyphen(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("-");
|
||||
}
|
||||
|
||||
fn ellipsis(&mut self, _cursor: Point, _layout: &TextLayout) {
|
||||
self.0.string("...");
|
||||
}
|
||||
|
||||
fn line_break(&mut self, _cursor: Point) {
|
||||
self.0.string("\n");
|
||||
}
|
||||
}
|
||||
|
||||
pub trait GlyphMetrics {
|
||||
fn char_width(&self, ch: char) -> i16;
|
||||
fn line_height(&self) -> i16;
|
||||
}
|
||||
|
||||
impl GlyphMetrics for Font {
|
||||
fn char_width(&self, ch: char) -> i16 {
|
||||
Font::char_width(*self, ch)
|
||||
}
|
||||
|
||||
fn line_height(&self) -> i16 {
|
||||
Font::line_height(*self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Carries info about the content that was processed
|
||||
/// on the current line.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Span {
|
||||
/// How many characters from the input text this span is laying out.
|
||||
pub length: usize,
|
||||
/// How many chars from the input text should we skip before fitting the
|
||||
/// next span?
|
||||
pub skip_next_chars: usize,
|
||||
/// By how much to offset the cursor after this span. If the vertical offset
|
||||
/// is bigger than zero, it means we are breaking the line.
|
||||
pub advance: Offset,
|
||||
/// If we are breaking the line, should we insert a hyphen right after this
|
||||
/// span to indicate a word-break?
|
||||
pub insert_hyphen_before_line_break: bool,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
fn fit_horizontally(
|
||||
text: &str,
|
||||
max_width: i16,
|
||||
text_font: impl GlyphMetrics,
|
||||
breaking: LineBreaking,
|
||||
) -> Self {
|
||||
const ASCII_LF: char = '\n';
|
||||
const ASCII_CR: char = '\r';
|
||||
const ASCII_SPACE: char = ' ';
|
||||
const ASCII_HYPHEN: char = '-';
|
||||
|
||||
fn is_whitespace(ch: char) -> bool {
|
||||
ch == ASCII_SPACE || ch == ASCII_LF || ch == ASCII_CR
|
||||
}
|
||||
|
||||
let hyphen_width = text_font.char_width(ASCII_HYPHEN);
|
||||
|
||||
// The span we return in case the line has to break. We mutate it in the
|
||||
// possible break points, and its initial value is returned in case no text
|
||||
// at all is fitting the constraints: zero length, zero width, full line
|
||||
// break.
|
||||
let mut line = Self {
|
||||
length: 0,
|
||||
advance: Offset::y(text_font.line_height()),
|
||||
insert_hyphen_before_line_break: false,
|
||||
skip_next_chars: 0,
|
||||
};
|
||||
|
||||
let mut span_width = 0;
|
||||
let mut found_any_whitespace = false;
|
||||
|
||||
let mut char_indices_iter = text.char_indices().peekable();
|
||||
// Iterating manually because we need a reference to the iterator inside the
|
||||
// loop.
|
||||
while let Some((i, ch)) = char_indices_iter.next() {
|
||||
let char_width = text_font.char_width(ch);
|
||||
|
||||
// Consider if we could be breaking the line at this position.
|
||||
if is_whitespace(ch) {
|
||||
// Break before the whitespace, without hyphen.
|
||||
line.length = i;
|
||||
line.advance.x = span_width;
|
||||
line.insert_hyphen_before_line_break = false;
|
||||
line.skip_next_chars = 1;
|
||||
if ch == ASCII_CR {
|
||||
// We'll be breaking the line, but advancing the cursor only by a half of the
|
||||
// regular line height.
|
||||
line.advance.y = text_font.line_height() / 2;
|
||||
}
|
||||
if ch == ASCII_LF || ch == ASCII_CR {
|
||||
// End of line, break immediately.
|
||||
return line;
|
||||
}
|
||||
found_any_whitespace = true;
|
||||
} else if span_width + char_width > max_width {
|
||||
// Cannot fit on this line. Return the last breakpoint.
|
||||
return line;
|
||||
} else {
|
||||
let have_space_for_break = span_width + char_width + hyphen_width <= max_width;
|
||||
let can_break_word = matches!(breaking, LineBreaking::BreakWordsAndInsertHyphen)
|
||||
|| !found_any_whitespace;
|
||||
if have_space_for_break && can_break_word {
|
||||
// Break after this character, append hyphen.
|
||||
line.length = match char_indices_iter.peek() {
|
||||
Some((idx, _)) => *idx,
|
||||
None => text.len(),
|
||||
};
|
||||
line.advance.x = span_width + char_width;
|
||||
line.insert_hyphen_before_line_break = true;
|
||||
line.skip_next_chars = 0;
|
||||
}
|
||||
}
|
||||
|
||||
span_width += char_width;
|
||||
}
|
||||
|
||||
// The whole text is fitting on the current line.
|
||||
Self {
|
||||
length: text.len(),
|
||||
advance: Offset::x(span_width),
|
||||
insert_hyphen_before_line_break: false,
|
||||
skip_next_chars: 0,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,47 @@
|
||||
mod bip39;
|
||||
mod button;
|
||||
mod button_controller;
|
||||
mod changing_text;
|
||||
mod choice;
|
||||
mod choice_item;
|
||||
mod common;
|
||||
mod confirm;
|
||||
mod dialog;
|
||||
mod flow;
|
||||
mod flow_pages;
|
||||
mod flow_pages_poc_helpers;
|
||||
mod frame;
|
||||
mod loader;
|
||||
mod page;
|
||||
mod passphrase;
|
||||
mod pin;
|
||||
mod result_anim;
|
||||
mod result_popup;
|
||||
mod scrollbar;
|
||||
mod simple_choice;
|
||||
|
||||
use super::theme;
|
||||
|
||||
pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet};
|
||||
pub use bip39::{Bip39Entry, Bip39EntryMsg};
|
||||
pub use button::{
|
||||
Button, ButtonAction, ButtonActions, ButtonContent, ButtonDetails, ButtonLayout, ButtonMsg,
|
||||
ButtonPos, ButtonStyle, ButtonStyleSheet,
|
||||
};
|
||||
pub use confirm::{HoldToConfirm, HoldToConfirmMsg};
|
||||
|
||||
pub use button_controller::{ButtonController, ButtonControllerMsg};
|
||||
pub use changing_text::ChangingTextLine;
|
||||
pub use choice::{ChoiceFactory, ChoicePage, ChoicePageMsg};
|
||||
pub use choice_item::{ChoiceItem, ChoiceItemAPI, MultilineTextChoiceItem, TextChoiceItem};
|
||||
pub use dialog::{Dialog, DialogMsg};
|
||||
pub use flow::{Flow, FlowMsg};
|
||||
pub use flow_pages::{FlowPages, Page};
|
||||
pub use frame::Frame;
|
||||
pub use loader::{Loader, LoaderMsg, LoaderStyle, LoaderStyleSheet};
|
||||
pub use page::ButtonPage;
|
||||
pub use passphrase::{PassphraseEntry, PassphraseEntryMsg};
|
||||
pub use pin::{PinEntry, PinEntryMsg};
|
||||
pub use result_anim::{ResultAnim, ResultAnimMsg};
|
||||
pub use result_popup::{ResultPopup, ResultPopupMsg};
|
||||
pub use scrollbar::ScrollBar;
|
||||
pub use simple_choice::{SimpleChoice, SimpleChoiceMsg};
|
||||
|
@ -0,0 +1,365 @@
|
||||
use crate::{
|
||||
time::Duration,
|
||||
ui::{
|
||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
|
||||
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, MultilineTextChoiceItem, TextChoiceItem,
|
||||
};
|
||||
use heapless::String;
|
||||
|
||||
pub enum PassphraseEntryMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Defines the choices currently available on the screen
|
||||
#[derive(PartialEq, Clone)]
|
||||
enum ChoiceCategory {
|
||||
Menu,
|
||||
LowercaseLetter,
|
||||
UppercaseLetter,
|
||||
Digit,
|
||||
SpecialSymbol,
|
||||
}
|
||||
|
||||
const MAX_PASSPHRASE_LENGTH: usize = 50;
|
||||
const HOLD_DURATION: Duration = Duration::from_secs(1);
|
||||
|
||||
const DIGITS: [char; 10] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const LOWERCASE_LETTERS: [char; 26] = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
|
||||
't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
const UPPERCASE_LETTERS: [char; 26] = [
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
|
||||
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||
];
|
||||
const SPECIAL_SYMBOLS: [char; 30] = [
|
||||
'_', '<', '>', '.', ':', '@', '/', '|', '\\', '!', '(', ')', '+', '%', '&', '-', '[', ']', '?',
|
||||
'{', '}', ',', '\'', '`', ';', '"', '~', '$', '^', '=',
|
||||
];
|
||||
const MENU_LENGTH: usize = 6;
|
||||
const DEL_INDEX: usize = MENU_LENGTH - 1;
|
||||
const SHOW_INDEX: usize = MENU_LENGTH - 2;
|
||||
const MENU: [&str; MENU_LENGTH] = ["abc", "ABC", "123", "*#_", "SHOW PASS", "DEL LAST CHAR"];
|
||||
|
||||
/// Get a character at a specified index for a specified category.
|
||||
fn get_char(current_category: &ChoiceCategory, index: u8) -> char {
|
||||
let index = index as usize;
|
||||
match current_category {
|
||||
ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS[index],
|
||||
ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS[index],
|
||||
ChoiceCategory::Digit => DIGITS[index],
|
||||
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS[index],
|
||||
ChoiceCategory::Menu => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return category from menu based on page index.
|
||||
fn get_category_from_menu(page_index: u8) -> ChoiceCategory {
|
||||
match page_index {
|
||||
0 => ChoiceCategory::LowercaseLetter,
|
||||
1 => ChoiceCategory::UppercaseLetter,
|
||||
2 => ChoiceCategory::Digit,
|
||||
3 => ChoiceCategory::SpecialSymbol,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many choices are available for a specified category.
|
||||
/// (does not count the extra MENU choice for characters)
|
||||
fn get_category_length(current_category: &ChoiceCategory) -> u8 {
|
||||
match current_category {
|
||||
ChoiceCategory::LowercaseLetter => LOWERCASE_LETTERS.len() as u8,
|
||||
ChoiceCategory::UppercaseLetter => UPPERCASE_LETTERS.len() as u8,
|
||||
ChoiceCategory::Digit => DIGITS.len() as u8,
|
||||
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS.len() as u8,
|
||||
ChoiceCategory::Menu => MENU.len() as u8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this index is the MENU index - the last one in the list.
|
||||
fn is_menu_choice(current_category: &ChoiceCategory, page_index: u8) -> bool {
|
||||
if let ChoiceCategory::Menu = current_category {
|
||||
unreachable!()
|
||||
}
|
||||
let category_length = get_category_length(current_category);
|
||||
page_index == category_length
|
||||
}
|
||||
|
||||
struct ChoiceFactoryPassphrase {
|
||||
current_category: ChoiceCategory,
|
||||
}
|
||||
|
||||
impl ChoiceFactoryPassphrase {
|
||||
fn new(current_category: ChoiceCategory) -> Self {
|
||||
Self { current_category }
|
||||
}
|
||||
|
||||
/// MENU choices with accept and cancel hold-to-confirm side buttons.
|
||||
fn get_menu_item(&self, choice_index: u8) -> ChoiceItem {
|
||||
let choice = MENU[choice_index as usize];
|
||||
let item =
|
||||
MultilineTextChoiceItem::new(String::from(choice), ButtonLayout::default_three_icons())
|
||||
.use_delimiter(' ');
|
||||
let mut menu_item = ChoiceItem::MultilineText(item);
|
||||
|
||||
// Including accept button on the left and cancel on the very right
|
||||
// TODO: could have some icons instead of the shortcut text
|
||||
if choice_index == 0 {
|
||||
menu_item.set_left_btn(Some(
|
||||
ButtonDetails::text("ACC").with_duration(HOLD_DURATION),
|
||||
));
|
||||
} else if choice_index == MENU.len() as u8 - 1 {
|
||||
menu_item.set_right_btn(Some(
|
||||
ButtonDetails::text("CAN").with_duration(HOLD_DURATION),
|
||||
));
|
||||
}
|
||||
|
||||
menu_item
|
||||
}
|
||||
|
||||
/// Character choices with a MENU choice at the end (visible from start) to
|
||||
/// return back
|
||||
fn get_character_item(&self, choice_index: u8) -> ChoiceItem {
|
||||
if is_menu_choice(&self.current_category, choice_index) {
|
||||
let menu_choice =
|
||||
TextChoiceItem::new("MENU", ButtonLayout::three_icons_middle_text("RETURN"));
|
||||
ChoiceItem::Text(menu_choice)
|
||||
} else {
|
||||
let ch = get_char(&self.current_category, choice_index);
|
||||
let char_choice = BigCharacterChoiceItem::new(ch, ButtonLayout::default_three_icons());
|
||||
ChoiceItem::BigCharacter(char_choice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChoiceFactory for ChoiceFactoryPassphrase {
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem {
|
||||
match self.current_category {
|
||||
ChoiceCategory::Menu => self.get_menu_item(choice_index),
|
||||
_ => self.get_character_item(choice_index),
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&self) -> u8 {
|
||||
let length = get_category_length(&self.current_category);
|
||||
// All non-MENU categories have an extra item for returning back to MENU
|
||||
match self.current_category {
|
||||
ChoiceCategory::Menu => length,
|
||||
_ => length + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component for entering a passphrase.
|
||||
pub struct PassphraseEntry {
|
||||
choice_page: ChoicePage<ChoiceFactoryPassphrase>,
|
||||
passphrase_dots: Child<ChangingTextLine<String<MAX_PASSPHRASE_LENGTH>>>,
|
||||
show_plain_passphrase: bool,
|
||||
textbox: TextBox<MAX_PASSPHRASE_LENGTH>,
|
||||
current_category: ChoiceCategory,
|
||||
menu_position: u8, // position in the menu so we can return back
|
||||
}
|
||||
|
||||
impl PassphraseEntry {
|
||||
pub fn new() -> Self {
|
||||
let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu);
|
||||
Self {
|
||||
choice_page: ChoicePage::new(menu_choices),
|
||||
passphrase_dots: Child::new(ChangingTextLine::center_mono(String::new())),
|
||||
show_plain_passphrase: false,
|
||||
textbox: TextBox::empty(),
|
||||
current_category: ChoiceCategory::Menu,
|
||||
menu_position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_passphrase_dots(&mut self, ctx: &mut EventCtx) {
|
||||
// TODO: when the passphrase is longer than fits the screen, we might show
|
||||
// ellipsis
|
||||
if self.show_plain_passphrase {
|
||||
let passphrase = String::from(self.passphrase());
|
||||
self.passphrase_dots.inner_mut().update_text(passphrase);
|
||||
} else {
|
||||
let mut dots: String<MAX_PASSPHRASE_LENGTH> = String::new();
|
||||
for _ in 0..self.textbox.len() {
|
||||
unwrap!(dots.push_str("*"));
|
||||
}
|
||||
self.passphrase_dots.inner_mut().update_text(dots);
|
||||
}
|
||||
self.passphrase_dots.request_complete_repaint(ctx);
|
||||
}
|
||||
|
||||
fn append_char(&mut self, ctx: &mut EventCtx, ch: char) {
|
||||
self.textbox.append(ctx, ch);
|
||||
}
|
||||
|
||||
fn delete_last_digit(&mut self, ctx: &mut EventCtx) {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
|
||||
/// Displaying the MENU
|
||||
fn show_menu_page(&mut self, ctx: &mut EventCtx) {
|
||||
let menu_choices = ChoiceFactoryPassphrase::new(ChoiceCategory::Menu);
|
||||
self.choice_page.reset(ctx, menu_choices, true, false);
|
||||
// Going back to the last MENU position before showing the MENU
|
||||
self.choice_page.set_page_counter(ctx, self.menu_position);
|
||||
}
|
||||
|
||||
/// Displaying the character category
|
||||
fn show_category_page(&mut self, ctx: &mut EventCtx) {
|
||||
let category_choices = ChoiceFactoryPassphrase::new(self.current_category.clone());
|
||||
self.choice_page.reset(ctx, category_choices, true, true);
|
||||
}
|
||||
|
||||
pub fn passphrase(&self) -> &str {
|
||||
self.textbox.content()
|
||||
}
|
||||
|
||||
fn is_full(&self) -> bool {
|
||||
self.textbox.is_full()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PassphraseEntry {
|
||||
type Msg = PassphraseEntryMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let passphrase_area_height = self.passphrase_dots.inner().needed_height();
|
||||
let (passphrase_area, choice_area) = bounds.split_top(passphrase_area_height);
|
||||
self.passphrase_dots.place(passphrase_area);
|
||||
self.choice_page.place(choice_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Any event when showing real passphrase should hide it
|
||||
if self.show_plain_passphrase {
|
||||
self.show_plain_passphrase = false;
|
||||
self.update_passphrase_dots(ctx);
|
||||
}
|
||||
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
|
||||
if self.current_category == ChoiceCategory::Menu {
|
||||
match msg {
|
||||
// Going to new category, applying some action or returning the result
|
||||
Some(ChoicePageMsg::Choice(page_counter)) => match page_counter as usize {
|
||||
DEL_INDEX => {
|
||||
self.delete_last_digit(ctx);
|
||||
self.update_passphrase_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
SHOW_INDEX => {
|
||||
self.show_plain_passphrase = true;
|
||||
self.update_passphrase_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
_ => {
|
||||
self.menu_position = page_counter;
|
||||
self.current_category = get_category_from_menu(page_counter);
|
||||
self.show_category_page(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
},
|
||||
Some(ChoicePageMsg::LeftMost) => return Some(PassphraseEntryMsg::Confirmed),
|
||||
Some(ChoicePageMsg::RightMost) => return Some(PassphraseEntryMsg::Cancelled),
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// Coming back to MENU or adding new character
|
||||
if let Some(ChoicePageMsg::Choice(page_counter)) = msg {
|
||||
if is_menu_choice(&self.current_category, page_counter) {
|
||||
self.current_category = ChoiceCategory::Menu;
|
||||
self.show_menu_page(ctx);
|
||||
ctx.request_paint();
|
||||
} else if !self.is_full() {
|
||||
let new_char = get_char(&self.current_category, page_counter);
|
||||
self.append_char(ctx, new_char);
|
||||
self.update_passphrase_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.passphrase_dots.paint();
|
||||
self.choice_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::{ButtonAction, ButtonPos};
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use crate::ui::util;
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for PassphraseEntry {
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => match self.current_category {
|
||||
ChoiceCategory::Menu => match self.choice_page.has_previous_choice() {
|
||||
true => ButtonAction::PrevPage.string(),
|
||||
false => ButtonAction::Confirm.string(),
|
||||
},
|
||||
_ => ButtonAction::PrevPage.string(),
|
||||
},
|
||||
ButtonPos::Right => match self.current_category {
|
||||
ChoiceCategory::Menu => match self.choice_page.has_next_choice() {
|
||||
true => ButtonAction::NextPage.string(),
|
||||
false => ButtonAction::Cancel.string(),
|
||||
},
|
||||
_ => ButtonAction::NextPage.string(),
|
||||
},
|
||||
ButtonPos::Middle => {
|
||||
let current_index = self.choice_page.page_index() as usize;
|
||||
match &self.current_category {
|
||||
ChoiceCategory::Menu => match current_index {
|
||||
DEL_INDEX => ButtonAction::Action("Del last char").string(),
|
||||
SHOW_INDEX => ButtonAction::Action("Show pass").string(),
|
||||
_ => ButtonAction::select_item(MENU[current_index]),
|
||||
},
|
||||
_ => {
|
||||
// There is "MENU" option at the end
|
||||
match self.choice_page.has_next_choice() {
|
||||
false => ButtonAction::Action("Back to MENU").string(),
|
||||
true => {
|
||||
let ch = match &self.current_category {
|
||||
ChoiceCategory::LowercaseLetter => {
|
||||
LOWERCASE_LETTERS[current_index]
|
||||
}
|
||||
ChoiceCategory::UppercaseLetter => {
|
||||
UPPERCASE_LETTERS[current_index]
|
||||
}
|
||||
ChoiceCategory::Digit => DIGITS[current_index],
|
||||
ChoiceCategory::SpecialSymbol => SPECIAL_SYMBOLS[current_index],
|
||||
ChoiceCategory::Menu => unreachable!(),
|
||||
};
|
||||
ButtonAction::select_item(util::char_to_string::<1>(ch))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("PassphraseEntry");
|
||||
// NOTE: `show_plain_passphrase` was not able to be transferred,
|
||||
// as it is true only for a very small amount of time
|
||||
t.kw_pair("textbox", self.textbox.content());
|
||||
self.report_btn_actions(t);
|
||||
t.field("choice_page", &self.choice_page);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
use crate::{
|
||||
micropython::buffer::StrBuffer,
|
||||
trezorhal::random,
|
||||
ui::{
|
||||
component::{text::common::TextBox, Child, Component, ComponentExt, Event, EventCtx},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
choice_item::BigCharacterChoiceItem, ButtonDetails, ButtonLayout, ChangingTextLine,
|
||||
ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, MultilineTextChoiceItem,
|
||||
};
|
||||
use heapless::String;
|
||||
|
||||
pub enum PinEntryMsg {
|
||||
Confirmed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
const MAX_PIN_LENGTH: usize = 50;
|
||||
const MAX_VISIBLE_DOTS: usize = 18;
|
||||
const MAX_VISIBLE_DIGITS: usize = 18;
|
||||
|
||||
const CHOICE_LENGTH: usize = 14;
|
||||
const EXIT_INDEX: usize = 0;
|
||||
const DELETE_INDEX: usize = 1;
|
||||
const SHOW_INDEX: usize = 2;
|
||||
const PROMPT_INDEX: usize = 3;
|
||||
const CHOICES: [&str; CHOICE_LENGTH] = [
|
||||
"EXIT",
|
||||
"DELETE",
|
||||
"SHOW PIN",
|
||||
"PLACEHOLDER FOR THE PROMPT",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
];
|
||||
|
||||
struct ChoiceFactoryPIN {
|
||||
prompt: StrBuffer,
|
||||
}
|
||||
|
||||
impl ChoiceFactoryPIN {
|
||||
fn new(prompt: StrBuffer) -> Self {
|
||||
Self { prompt }
|
||||
}
|
||||
}
|
||||
|
||||
impl ChoiceFactory for ChoiceFactoryPIN {
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem {
|
||||
let choice = CHOICES[choice_index as usize];
|
||||
|
||||
// Depending on whether it is a digit (one character) or a text.
|
||||
// Digits are BIG, the rest is multiline.
|
||||
let mut choice_item = if choice.len() == 1 {
|
||||
let item =
|
||||
BigCharacterChoiceItem::from_str(choice, ButtonLayout::default_three_icons());
|
||||
ChoiceItem::BigCharacter(item)
|
||||
} else {
|
||||
let item = MultilineTextChoiceItem::new(
|
||||
String::from(choice),
|
||||
ButtonLayout::default_three_icons(),
|
||||
)
|
||||
.use_delimiter(' ');
|
||||
ChoiceItem::MultilineText(item)
|
||||
};
|
||||
|
||||
// Action buttons have different middle button text
|
||||
if [EXIT_INDEX, DELETE_INDEX, SHOW_INDEX, PROMPT_INDEX].contains(&(choice_index as usize)) {
|
||||
let confirm_btn = ButtonDetails::armed_text("CONFIRM");
|
||||
choice_item.set_middle_btn(Some(confirm_btn));
|
||||
}
|
||||
|
||||
// Changing the prompt text for the wanted one
|
||||
if choice_index == PROMPT_INDEX as u8 {
|
||||
choice_item.set_text(String::from(self.prompt.as_ref()));
|
||||
}
|
||||
|
||||
choice_item
|
||||
}
|
||||
|
||||
fn count(&self) -> u8 {
|
||||
CHOICE_LENGTH as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Component for entering a PIN.
|
||||
pub struct PinEntry {
|
||||
choice_page: ChoicePage<ChoiceFactoryPIN>,
|
||||
pin_dots: Child<ChangingTextLine<String<MAX_PIN_LENGTH>>>,
|
||||
show_real_pin: bool,
|
||||
textbox: TextBox<MAX_PIN_LENGTH>,
|
||||
}
|
||||
|
||||
impl PinEntry {
|
||||
pub fn new(prompt: StrBuffer) -> Self {
|
||||
let choices = ChoiceFactoryPIN::new(prompt);
|
||||
|
||||
Self {
|
||||
choice_page: ChoicePage::new(choices)
|
||||
.with_initial_page_counter(3)
|
||||
.with_carousel(),
|
||||
pin_dots: Child::new(ChangingTextLine::center_mono(String::new())),
|
||||
show_real_pin: false,
|
||||
textbox: TextBox::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_new_digit(&mut self, ctx: &mut EventCtx, page_counter: u8) {
|
||||
let digit = CHOICES[page_counter as usize];
|
||||
self.textbox.append_slice(ctx, digit);
|
||||
}
|
||||
|
||||
fn delete_last_digit(&mut self, ctx: &mut EventCtx) {
|
||||
self.textbox.delete_last(ctx);
|
||||
}
|
||||
|
||||
fn update_pin_dots(&mut self, ctx: &mut EventCtx) {
|
||||
// TODO: this is the same action as for the passphrase entry,
|
||||
// might do a common component that will handle this part,
|
||||
// (something like `SecretTextLine`)
|
||||
// also with things like shifting the dots when too many etc.
|
||||
// TODO: when the PIN is longer than fits the screen, we might show ellipsis
|
||||
if self.show_real_pin {
|
||||
let pin = String::from(self.pin());
|
||||
self.pin_dots.inner_mut().update_text(pin);
|
||||
} else {
|
||||
let mut dots: String<MAX_PIN_LENGTH> = String::new();
|
||||
for _ in 0..self.textbox.len() {
|
||||
unwrap!(dots.push_str("*"));
|
||||
}
|
||||
self.pin_dots.inner_mut().update_text(dots);
|
||||
}
|
||||
self.pin_dots.request_complete_repaint(ctx);
|
||||
}
|
||||
|
||||
pub fn pin(&self) -> &str {
|
||||
self.textbox.content()
|
||||
}
|
||||
|
||||
fn is_full(&self) -> bool {
|
||||
self.textbox.is_full()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.textbox.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for PinEntry {
|
||||
type Msg = PinEntryMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let pin_area_height = self.pin_dots.inner().needed_height();
|
||||
let (pin_area, choice_area) = bounds.split_top(pin_area_height);
|
||||
self.pin_dots.place(pin_area);
|
||||
self.choice_page.place(choice_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Any event when showing real PIN should hide it
|
||||
if self.show_real_pin {
|
||||
self.show_real_pin = false;
|
||||
self.update_pin_dots(ctx);
|
||||
}
|
||||
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
if let Some(ChoicePageMsg::Choice(page_counter)) = msg {
|
||||
// Performing action under specific index or appending new digit
|
||||
match page_counter as usize {
|
||||
EXIT_INDEX => return Some(PinEntryMsg::Cancelled),
|
||||
DELETE_INDEX => {
|
||||
self.delete_last_digit(ctx);
|
||||
self.update_pin_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
SHOW_INDEX => {
|
||||
self.show_real_pin = true;
|
||||
self.update_pin_dots(ctx);
|
||||
ctx.request_paint();
|
||||
}
|
||||
PROMPT_INDEX => return Some(PinEntryMsg::Confirmed),
|
||||
_ => {
|
||||
if !self.is_full() {
|
||||
self.append_new_digit(ctx, page_counter);
|
||||
self.update_pin_dots(ctx);
|
||||
// Choosing any random digit to be shown next
|
||||
let new_page_counter =
|
||||
random::uniform_between(4, (CHOICE_LENGTH - 1) as u32);
|
||||
self.choice_page
|
||||
.set_page_counter(ctx, new_page_counter as u8);
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pin_dots.paint();
|
||||
self.choice_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::{ButtonAction, ButtonPos};
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for PinEntry {
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => ButtonAction::PrevPage.string(),
|
||||
ButtonPos::Right => ButtonAction::NextPage.string(),
|
||||
ButtonPos::Middle => {
|
||||
let current_index = self.choice_page.page_index() as usize;
|
||||
match current_index {
|
||||
EXIT_INDEX => ButtonAction::Cancel.string(),
|
||||
DELETE_INDEX => ButtonAction::Action("Delete last digit").string(),
|
||||
SHOW_INDEX => ButtonAction::Action("Show PIN").string(),
|
||||
PROMPT_INDEX => ButtonAction::Confirm.string(),
|
||||
_ => ButtonAction::select_item(CHOICES[current_index]),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("PinEntry");
|
||||
// NOTE: `show_real_pin` was not able to be transferred,
|
||||
// as it is true only for a very small amount of time
|
||||
t.kw_pair("textbox", self.textbox.content());
|
||||
self.report_btn_actions(t);
|
||||
t.field("choice_page", &self.choice_page);
|
||||
t.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,165 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never, Pad},
|
||||
display,
|
||||
geometry::{Offset, Point, Rect},
|
||||
model_tr::theme,
|
||||
};
|
||||
|
||||
/// In which direction should the scrollbar be positioned
|
||||
pub enum ScrollbarOrientation {
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
|
||||
pub struct ScrollBar {
|
||||
area: Rect,
|
||||
pad: Pad,
|
||||
pub page_count: usize,
|
||||
pub active_page: usize,
|
||||
pub orientation: ScrollbarOrientation,
|
||||
}
|
||||
|
||||
impl ScrollBar {
|
||||
pub const WIDTH: i16 = 8;
|
||||
pub const DOT_SIZE: Offset = Offset::new(4, 4);
|
||||
pub const DOT_INTERVAL: i16 = 6;
|
||||
|
||||
pub fn new(page_count: usize, orientation: ScrollbarOrientation) -> Self {
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
pad: Pad::with_background(theme::BG),
|
||||
page_count,
|
||||
active_page: 0,
|
||||
orientation,
|
||||
}
|
||||
}
|
||||
|
||||
/// Page count will be given later as it is not available yet.
|
||||
pub fn vertical_to_be_filled_later() -> Self {
|
||||
Self::vertical(0)
|
||||
}
|
||||
|
||||
pub fn vertical(page_count: usize) -> Self {
|
||||
Self::new(page_count, ScrollbarOrientation::Vertical)
|
||||
}
|
||||
|
||||
pub fn horizontal(page_count: usize) -> Self {
|
||||
Self::new(page_count, ScrollbarOrientation::Horizontal)
|
||||
}
|
||||
|
||||
pub fn set_page_count(&mut self, page_count: usize) {
|
||||
self.page_count = page_count;
|
||||
}
|
||||
|
||||
pub fn set_active_page(&mut self, active_page: usize) {
|
||||
self.active_page = active_page;
|
||||
}
|
||||
|
||||
pub fn has_next_page(&self) -> bool {
|
||||
self.active_page < self.page_count - 1
|
||||
}
|
||||
|
||||
pub fn has_previous_page(&self) -> bool {
|
||||
self.active_page > 0
|
||||
}
|
||||
|
||||
pub fn go_to_next_page(&mut self) {
|
||||
self.active_page = self.active_page.saturating_add(1).min(self.page_count - 1);
|
||||
}
|
||||
|
||||
pub fn go_to_previous_page(&mut self) {
|
||||
self.active_page = self.active_page.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Create a (seemingly circular) dot given its top left point.
|
||||
/// Make it full when it is active, otherwise paint just the perimeter and
|
||||
/// leave center empty.
|
||||
fn paint_dot(&self, active: bool, top_left: Point) {
|
||||
let full_square = Rect::from_top_left_and_size(top_left, ScrollBar::DOT_SIZE);
|
||||
|
||||
// FG - painting the full square
|
||||
display::rect_fill(full_square, theme::FG);
|
||||
|
||||
// BG - erase four corners
|
||||
for p in full_square.corner_points().iter() {
|
||||
display::paint_point(p, theme::BG);
|
||||
}
|
||||
|
||||
// BG - erasing the middle when not active
|
||||
if !active {
|
||||
display::rect_fill(full_square.shrink(1), theme::BG)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_vertical(&mut self) {
|
||||
let count = self.page_count as i16;
|
||||
let interval = {
|
||||
let available_space = self.area.height();
|
||||
let naive_space = count * Self::DOT_INTERVAL;
|
||||
if naive_space > available_space {
|
||||
available_space / count
|
||||
} else {
|
||||
Self::DOT_INTERVAL
|
||||
}
|
||||
};
|
||||
let mut top_left = Point::new(
|
||||
self.area.center().x - Self::DOT_SIZE.x / 2,
|
||||
self.area.center().y - (count / 2) * interval,
|
||||
);
|
||||
for i in 0..self.page_count {
|
||||
self.paint_dot(i == self.active_page, top_left);
|
||||
top_left.y += interval;
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_horizontal(&mut self) {
|
||||
let count = self.page_count as i16;
|
||||
let interval = {
|
||||
let available_space = self.area.width();
|
||||
let naive_space = count * Self::DOT_INTERVAL;
|
||||
if naive_space > available_space {
|
||||
available_space / count
|
||||
} else {
|
||||
Self::DOT_INTERVAL
|
||||
}
|
||||
};
|
||||
let mut top_left = Point::new(
|
||||
self.area.center().x - (count / 2) * interval,
|
||||
self.area.center().y - Self::DOT_SIZE.y / 2,
|
||||
);
|
||||
for i in 0..self.page_count {
|
||||
self.paint_dot(i == self.active_page, top_left);
|
||||
top_left.x += interval;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ScrollBar {
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
self.pad.place(bounds);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Displaying one dot for each page.
|
||||
fn paint(&mut self) {
|
||||
// Not showing the scrollbar dot when there is only one page
|
||||
if self.page_count <= 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.pad.clear();
|
||||
self.pad.paint();
|
||||
if matches!(self.orientation, ScrollbarOrientation::Vertical) {
|
||||
self.paint_vertical()
|
||||
} else {
|
||||
self.paint_horizontal()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
use super::{ButtonLayout, ChoiceFactory, ChoiceItem, ChoicePage, ChoicePageMsg, TextChoiceItem};
|
||||
use heapless::{String, Vec};
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
use super::{ButtonAction, ButtonPos};
|
||||
|
||||
pub enum SimpleChoiceMsg {
|
||||
Result(String<50>),
|
||||
}
|
||||
|
||||
struct ChoiceFactorySimple<T, const N: usize> {
|
||||
choices: Vec<T, N>,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> ChoiceFactorySimple<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn new(choices: Vec<T, N>) -> Self {
|
||||
Self { choices }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> ChoiceFactory for ChoiceFactorySimple<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
fn get(&self, choice_index: u8) -> ChoiceItem {
|
||||
let text = &self.choices[choice_index as usize];
|
||||
let text_item = TextChoiceItem::new(text, ButtonLayout::default_three_icons());
|
||||
let mut choice_item = ChoiceItem::Text(text_item);
|
||||
|
||||
// Disabling prev/next buttons for the first/last choice.
|
||||
if choice_index == 0 {
|
||||
choice_item.set_left_btn(None);
|
||||
} else if choice_index as usize == N - 1 {
|
||||
choice_item.set_right_btn(None);
|
||||
}
|
||||
|
||||
choice_item
|
||||
}
|
||||
|
||||
fn count(&self) -> u8 {
|
||||
N as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple wrapper around `ChoicePage` that allows for
|
||||
/// inputting a list of values and receiving the chosen one.
|
||||
pub struct SimpleChoice<T, const N: usize>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
choices: Vec<T, N>,
|
||||
choice_page: ChoicePage<ChoiceFactorySimple<T, N>>,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> SimpleChoice<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
pub fn new(str_choices: Vec<T, N>) -> Self {
|
||||
let choices = ChoiceFactorySimple::new(str_choices.clone());
|
||||
Self {
|
||||
choices: str_choices,
|
||||
choice_page: ChoicePage::new(choices),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> Component for SimpleChoice<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
type Msg = SimpleChoiceMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.choice_page.place(bounds)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let msg = self.choice_page.event(ctx, event);
|
||||
match msg {
|
||||
Some(ChoicePageMsg::Choice(page_counter)) => {
|
||||
let result = String::from(self.choices[page_counter as usize].as_ref());
|
||||
Some(SimpleChoiceMsg::Result(result))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.choice_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, const N: usize> crate::trace::Trace for SimpleChoice<T, N>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
T: Clone,
|
||||
{
|
||||
fn get_btn_action(&self, pos: ButtonPos) -> String<25> {
|
||||
match pos {
|
||||
ButtonPos::Left => match self.choice_page.has_previous_choice() {
|
||||
true => ButtonAction::PrevPage.string(),
|
||||
false => ButtonAction::empty(),
|
||||
},
|
||||
ButtonPos::Right => match self.choice_page.has_next_choice() {
|
||||
true => ButtonAction::NextPage.string(),
|
||||
false => ButtonAction::empty(),
|
||||
},
|
||||
ButtonPos::Middle => {
|
||||
let current_index = self.choice_page.page_index() as usize;
|
||||
ButtonAction::select_item(self.choices[current_index].as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.open("SimpleChoice");
|
||||
self.report_btn_actions(t);
|
||||
t.field("choice_page", &self.choice_page);
|
||||
t.close();
|
||||
}
|
||||
}
|