@ -0,0 +1 @@
|
||||
Implement UI for Model R
|
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 479 B |
After Width: | Height: | Size: 133 B |
After Width: | Height: | Size: 175 B |
After Width: | Height: | Size: 164 B |
After Width: | Height: | Size: 125 B |
After Width: | Height: | Size: 134 B |
After Width: | Height: | Size: 162 B |
After Width: | Height: | Size: 129 B |
After Width: | Height: | Size: 139 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: 6.9 KiB |
After Width: | Height: | Size: 139 B |
After Width: | Height: | Size: 159 B |
After Width: | Height: | Size: 155 B |
After Width: | Height: | Size: 137 B |
After Width: | Height: | Size: 165 B |
After Width: | Height: | Size: 156 B |
After Width: | Height: | Size: 165 B |
After Width: | Height: | Size: 118 B |
After Width: | Height: | Size: 117 B |
After Width: | Height: | Size: 164 B |
After Width: | Height: | Size: 166 B |
After Width: | Height: | Size: 176 B |
After Width: | Height: | Size: 131 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, 8, 0, 0 };
|
||||
/* ! */ 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,10 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#if TREZOR_FONT_BPP != 1
|
||||
#error Wrong TREZOR_FONT_BPP (expected 1)
|
||||
#endif
|
||||
#define Font_Unifont_Bold_16_HEIGHT 12 // <--- 12 from 16
|
||||
#define Font_Unifont_Bold_16_MAX_HEIGHT 12 // <--- 12 from 15
|
||||
#define Font_Unifont_Bold_16_BASELINE 2
|
||||
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,207 @@
|
||||
#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
|
||||
|
||||
// MANUAL CHANGES!
|
||||
// In cases where the width and advance were the same (usually 7 and 7), increasing
|
||||
// the advance to 8, so that these wide letters do not collide with the following one.
|
||||
|
||||
/* */ 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, 8, 0, 10, 16, 250, 76, 135, 3, 132, 201, 124, 32 }; // < --- advanced changed from 7 to 8
|
||||
/* % */ static const uint8_t Font_Unifont_Regular_16_glyph_37[] = { 7, 10, 8, 0, 10, 99, 42, 83, 65, 2, 11, 41, 83, 24 }; // < --- advanced changed from 7 to 8
|
||||
/* & */ static const uint8_t Font_Unifont_Regular_16_glyph_38[] = { 7, 10, 8, 0, 10, 56, 137, 17, 67, 10, 98, 194, 140, 228 }; // < --- advanced changed from 7 to 8
|
||||
/* ' */ 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, 8, 0, 8, 17, 37, 81, 197, 82, 68, 0 }; // < --- advanced changed from 7 to 8
|
||||
/* + */ static const uint8_t Font_Unifont_Regular_16_glyph_43[] = { 7, 7, 8, 0, 8, 16, 32, 71, 241, 2, 4, 0 }; // < --- advanced changed from 7 to 8
|
||||
/* , */ 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, 8, 0, 10, 62, 16, 32, 64, 129, 2, 68, 136, 224 }; // < --- advanced changed from 7 to 8
|
||||
/* 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, 8, 0, 10, 121, 10, 20, 40, 80, 161, 90, 204, 240, 24 }; // < --- advanced changed from 7 to 8
|
||||
/* 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, 8, 0, 10, 254, 32, 64, 129, 2, 4, 8, 16, 32 }; // < --- advanced changed from 7 to 8
|
||||
/* 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, 8, 0, 10, 131, 6, 10, 36, 72, 138, 20, 16, 32 }; // < --- advanced changed from 7 to 8
|
||||
/* 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, 8, 0, 10, 131, 5, 18, 34, 130, 4, 8, 16, 32 }; // < --- advanced changed from 7 to 8
|
||||
/* 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, 8, 0, 0, 254 }; // < --- advanced changed from 7 to 8
|
||||
/* ` */ 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, 8, 0, 8, 237, 38, 76, 153, 50, 100, 201, 0 }; // < --- advanced changed from 7 to 8
|
||||
/* 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, 8, 0, 8, 131, 38, 76, 153, 50, 100, 182, 0 }; // < --- advanced changed from 7 to 8
|
||||
/* 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, 8, 0, 11, 99, 38, 48 }; // < --- advanced changed from 7 to 8
|
||||
|
||||
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,10 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#if TREZOR_FONT_BPP != 1
|
||||
#error Wrong TREZOR_FONT_BPP (expected 1)
|
||||
#endif
|
||||
#define Font_Unifont_Regular_16_HEIGHT 12 // <--- 12 from 16
|
||||
#define Font_Unifont_Regular_16_MAX_HEIGHT 12 // <--- 12 from 15
|
||||
#define Font_Unifont_Regular_16_BASELINE 2
|
||||
extern const uint8_t* const Font_Unifont_Regular_16[126 + 1 - 32];
|
||||
extern const uint8_t Font_Unifont_Regular_16_glyph_nonprintable[];
|
After Width: | Height: | Size: 431 KiB |
@ -1,31 +1,25 @@
|
||||
use cstr_core::CStr;
|
||||
use cstr_core::cstr;
|
||||
|
||||
use crate::{error::Error, micropython::qstr::Qstr};
|
||||
|
||||
// XXX const version of `from_bytes_with_nul_unchecked` is nightly-only.
|
||||
|
||||
pub fn experimental_not_enabled() -> Error {
|
||||
let msg =
|
||||
unsafe { CStr::from_bytes_with_nul_unchecked(b"Experimental features are disabled.\0") };
|
||||
Error::ValueError(msg)
|
||||
value_error!("Experimental features are disabled.")
|
||||
}
|
||||
|
||||
pub fn unknown_field_type() -> Error {
|
||||
let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Unknown field type.\0") };
|
||||
Error::ValueError(msg)
|
||||
value_error!("Unknown field type.")
|
||||
}
|
||||
|
||||
pub fn missing_required_field(field: Qstr) -> Error {
|
||||
let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Missing required field\0") };
|
||||
Error::ValueErrorParam(msg, field.into())
|
||||
Error::ValueErrorParam(cstr!("Missing required field."), field.into())
|
||||
}
|
||||
|
||||
pub fn invalid_value(field: Qstr) -> Error {
|
||||
let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"Invalid value for field\0") };
|
||||
Error::ValueErrorParam(msg, field.into())
|
||||
Error::ValueErrorParam(cstr!("Invalid value for field."), field.into())
|
||||
}
|
||||
|
||||
pub fn end_of_buffer() -> Error {
|
||||
let msg = unsafe { CStr::from_bytes_with_nul_unchecked(b"End of buffer.\0") };
|
||||
Error::ValueError(msg)
|
||||
value_error!("End of buffer.")
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
pub mod common;
|
||||
pub mod formatted;
|
||||
pub mod layout;
|
||||
pub mod op;
|
||||
pub mod paragraphs;
|
||||
pub mod util;
|
||||
|
||||
pub use layout::{LineBreaking, PageBreaking, TextStyle};
|
||||
|
@ -0,0 +1,240 @@
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
ui::{
|
||||
display::{Color, Font},
|
||||
geometry::{Alignment, Offset, Rect},
|
||||
util::ResultExt,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
layout::{LayoutFit, LayoutSink, TextLayout},
|
||||
LineBreaking, TextStyle,
|
||||
};
|
||||
|
||||
use heapless::Vec;
|
||||
|
||||
// So that there is only one implementation, and not multiple generic ones
|
||||
// as would be via `const N: usize` generics.
|
||||
const MAX_OPS: usize = 15;
|
||||
|
||||
/// To account for operations that are not made of characters
|
||||
/// but need to be accounted for somehow.
|
||||
/// Number of processed characters will be increased by this
|
||||
/// to account for the operation.
|
||||
const PROCESSED_CHARS_ONE: usize = 1;
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Extension of TextLayout, allowing for Op-based operations
|
||||
pub struct OpTextLayout<T: StringType + Clone> {
|
||||
pub layout: TextLayout,
|
||||
ops: Vec<Op<T>, MAX_OPS>,
|
||||
}
|
||||
|
||||
impl<'a, T: StringType + Clone + 'a> OpTextLayout<T> {
|
||||
pub fn new(style: TextStyle) -> Self {
|
||||
Self {
|
||||
layout: TextLayout::new(style),
|
||||
ops: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.layout.bounds = bounds;
|
||||
bounds
|
||||
}
|
||||
|
||||
/// Send the layout's content into a sink.
|
||||
pub fn layout_content(&mut self, skip_bytes: usize, sink: &mut dyn LayoutSink) -> LayoutFit {
|
||||
self.layout_ops(skip_bytes, sink)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn layout_ops(&mut self, skip_bytes: usize, sink: &mut dyn LayoutSink) -> LayoutFit {
|
||||
// TODO: make sure it is called when we have the current font (not sooner)
|
||||
let mut cursor = &mut self.layout.initial_cursor();
|
||||
let init_cursor = *cursor;
|
||||
let mut total_processed_chars = 0;
|
||||
|
||||
// Do something when it was not skipped
|
||||
for op in Self::filter_skipped_ops(self.ops.iter(), skip_bytes) {
|
||||
match op {
|
||||
// Changing color
|
||||
Op::Color(color) => {
|
||||
self.layout.style.text_color = color;
|
||||
}
|
||||
// Changing font
|
||||
Op::Font(font) => {
|
||||
self.layout.style.text_font = font;
|
||||
}
|
||||
// Changing line/text alignment
|
||||
Op::Alignment(line_alignment) => {
|
||||
self.layout.align = line_alignment;
|
||||
}
|
||||
// Changing line breaking
|
||||
Op::LineBreaking(line_breaking) => {
|
||||
self.layout.style.line_breaking = line_breaking;
|
||||
}
|
||||
// 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
|
||||
total_processed_chars += PROCESSED_CHARS_ONE;
|
||||
return LayoutFit::OutOfBounds {
|
||||
processed_chars: total_processed_chars,
|
||||
height: self.layout.layout_height(init_cursor, *cursor),
|
||||
};
|
||||
}
|
||||
// Drawing text
|
||||
Op::Text(text) => {
|
||||
// Try to fit text on the current page and if they do not fit,
|
||||
// return the appropriate OutOfBounds message
|
||||
|
||||
let fit = self.layout.layout_text(text.as_ref(), cursor, sink);
|
||||
|
||||
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.layout_height(init_cursor, *cursor),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LayoutFit::Fitting {
|
||||
processed_chars: total_processed_chars,
|
||||
height: self.layout.layout_height(init_cursor, *cursor),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets rid of all action-Ops that are before the `skip_bytes` threshold.
|
||||
/// (Not removing the style changes, e.g. Font or Color, because they need
|
||||
/// to be correctly set for future Text operations.)
|
||||
fn filter_skipped_ops<'b, I>(ops_iter: I, skip_bytes: usize) -> impl Iterator<Item = Op<T>> + 'b
|
||||
where
|
||||
I: Iterator<Item = &'b Op<T>> + 'b,
|
||||
'a: 'b,
|
||||
{
|
||||
let mut skipped = 0;
|
||||
ops_iter.filter_map(move |op| {
|
||||
match op {
|
||||
Op::Text(text) if skipped < skip_bytes => {
|
||||
let skip_text_bytes_if_fits_partially = skip_bytes - skipped;
|
||||
skipped = skipped.saturating_add(text.as_ref().len());
|
||||
if skipped > skip_bytes {
|
||||
// Fits partially
|
||||
// Skipping some bytes at the beginning, leaving rest
|
||||
Some(Op::Text(
|
||||
text.skip_prefix(skip_text_bytes_if_fits_partially),
|
||||
))
|
||||
} else {
|
||||
// Does not fit at all
|
||||
None
|
||||
}
|
||||
}
|
||||
Op::NextPage if skipped < skip_bytes => {
|
||||
skipped = skipped.saturating_add(PROCESSED_CHARS_ONE);
|
||||
None
|
||||
}
|
||||
Op::CursorOffset(_) if skipped < skip_bytes => {
|
||||
// Skip any offsets
|
||||
None
|
||||
}
|
||||
op_to_pass_through => Some(op_to_pass_through.clone()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Op-adding operations
|
||||
impl<T: StringType + Clone> OpTextLayout<T> {
|
||||
pub fn with_new_item(mut self, item: Op<T>) -> Self {
|
||||
self.ops
|
||||
.push(item)
|
||||
.assert_if_debugging_ui("Could not push to self.ops - increase MAX_OPS.");
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text(self, text: T) -> Self {
|
||||
self.with_new_item(Op::Text(text))
|
||||
}
|
||||
|
||||
pub fn newline(self) -> Self {
|
||||
self.with_new_item(Op::Text("\n".into()))
|
||||
}
|
||||
|
||||
pub fn newline_half(self) -> Self {
|
||||
self.with_new_item(Op::Text("\r".into()))
|
||||
}
|
||||
|
||||
pub fn next_page(self) -> Self {
|
||||
self.with_new_item(Op::NextPage)
|
||||
}
|
||||
|
||||
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: Alignment) -> Self {
|
||||
self.with_new_item(Op::Alignment(alignment))
|
||||
}
|
||||
|
||||
pub fn line_breaking(self, line_breaking: LineBreaking) -> Self {
|
||||
self.with_new_item(Op::LineBreaking(line_breaking))
|
||||
}
|
||||
}
|
||||
|
||||
// Op-adding aggregation operations
|
||||
impl<T: StringType + Clone> OpTextLayout<T> {
|
||||
pub fn text_normal(self, text: T) -> Self {
|
||||
self.font(Font::NORMAL).text(text)
|
||||
}
|
||||
|
||||
pub fn text_mono(self, text: T) -> Self {
|
||||
self.font(Font::MONO).text(text)
|
||||
}
|
||||
|
||||
pub fn text_bold(self, text: T) -> Self {
|
||||
self.font(Font::BOLD).text(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Op<T: StringType> {
|
||||
/// Render text with current color and font.
|
||||
Text(T),
|
||||
/// Set current text color.
|
||||
Color(Color),
|
||||
/// Set currently used font.
|
||||
Font(Font),
|
||||
/// Set currently used line alignment.
|
||||
Alignment(Alignment),
|
||||
/// Set currently used line breaking algorithm.
|
||||
LineBreaking(LineBreaking),
|
||||
/// Move the current cursor by specified Offset.
|
||||
CursorOffset(Offset),
|
||||
/// Force continuing on the next page.
|
||||
NextPage,
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
use crate::ui::{
|
||||
display::{Color, Font},
|
||||
geometry::{Alignment, Rect},
|
||||
};
|
||||
|
||||
use super::{
|
||||
layout::{LayoutFit, TextLayout},
|
||||
TextStyle,
|
||||
};
|
||||
|
||||
/// Draws longer multiline texts inside an area.
|
||||
/// Splits lines on word boundaries/whitespace.
|
||||
/// When a word is too long to fit one line, splitting
|
||||
/// it on multiple lines with "-" at the line-ends.
|
||||
///
|
||||
/// If it fits, returns the rest of the area.
|
||||
/// If it does not fit, returns `None`.
|
||||
pub fn text_multiline_split_words(
|
||||
area: Rect,
|
||||
text: &str,
|
||||
font: Font,
|
||||
fg_color: Color,
|
||||
bg_color: Color,
|
||||
alignment: Alignment,
|
||||
) -> Option<Rect> {
|
||||
let text_style = TextStyle::new(font, fg_color, bg_color, fg_color, fg_color);
|
||||
let text_layout = TextLayout::new(text_style)
|
||||
.with_bounds(area)
|
||||
.with_align(alignment);
|
||||
let layout_fit = text_layout.render_text(text);
|
||||
match layout_fit {
|
||||
LayoutFit::Fitting { height, .. } => Some(area.split_top(height).1),
|
||||
LayoutFit::OutOfBounds { .. } => None,
|
||||
}
|
||||
}
|
@ -0,0 +1,280 @@
|
||||
use heapless::Vec;
|
||||
|
||||
use crate::{
|
||||
error::Error,
|
||||
strutil::StringType,
|
||||
ui::{
|
||||
component::{
|
||||
text::paragraphs::{Paragraph, ParagraphSource, ParagraphVecShort, Paragraphs, VecExt},
|
||||
Child, Component, Event, EventCtx, Pad, Paginate, Qr,
|
||||
},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
theme, ButtonController, ButtonControllerMsg, ButtonDetails, ButtonLayout, ButtonPos, Frame,
|
||||
};
|
||||
|
||||
const MAX_XPUBS: usize = 16;
|
||||
const QR_BORDER: i16 = 3;
|
||||
|
||||
pub struct AddressDetails<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
qr_code: Qr,
|
||||
details_view: Paragraphs<ParagraphVecShort<T>>,
|
||||
xpub_view: Frame<Paragraphs<Paragraph<T>>, T>,
|
||||
xpubs: Vec<(T, T), MAX_XPUBS>,
|
||||
current_page: usize,
|
||||
current_subpage: usize,
|
||||
area: Rect,
|
||||
pad: Pad,
|
||||
buttons: Child<ButtonController<T>>,
|
||||
}
|
||||
|
||||
impl<T> AddressDetails<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
pub fn new(
|
||||
qr_address: T,
|
||||
case_sensitive: bool,
|
||||
account: Option<T>,
|
||||
path: Option<T>,
|
||||
) -> Result<Self, Error> {
|
||||
let qr_code = Qr::new(qr_address, case_sensitive)?.with_border(QR_BORDER);
|
||||
let details_view = {
|
||||
let mut para = ParagraphVecShort::new();
|
||||
if let Some(account) = account {
|
||||
para.add(Paragraph::new(&theme::TEXT_BOLD, "Account:".into()));
|
||||
para.add(Paragraph::new(&theme::TEXT_MONO, account));
|
||||
}
|
||||
if let Some(path) = path {
|
||||
para.add(Paragraph::new(&theme::TEXT_BOLD, "Derivation path:".into()));
|
||||
para.add(Paragraph::new(&theme::TEXT_MONO, path));
|
||||
}
|
||||
Paragraphs::new(para)
|
||||
};
|
||||
let xpub_view = Frame::new(
|
||||
"".into(),
|
||||
Paragraph::new(&theme::TEXT_MONO_DATA, "".into()).into_paragraphs(),
|
||||
);
|
||||
|
||||
let result = Self {
|
||||
qr_code,
|
||||
details_view,
|
||||
xpub_view,
|
||||
xpubs: Vec::new(),
|
||||
area: Rect::zero(),
|
||||
current_page: 0,
|
||||
current_subpage: 0,
|
||||
pad: Pad::with_background(theme::BG).with_clear(),
|
||||
buttons: Child::new(ButtonController::new(ButtonLayout::arrow_none_arrow())),
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn add_xpub(&mut self, title: T, xpub: T) -> Result<(), Error> {
|
||||
self.xpubs
|
||||
.push((title, xpub))
|
||||
.map_err(|_| Error::OutOfRange)
|
||||
}
|
||||
|
||||
fn is_in_subpage(&self) -> bool {
|
||||
self.current_subpage > 0
|
||||
}
|
||||
|
||||
fn is_xpub_page(&self) -> bool {
|
||||
self.current_page > 1
|
||||
}
|
||||
|
||||
fn is_last_page(&self) -> bool {
|
||||
self.current_page == self.page_count() - 1
|
||||
}
|
||||
|
||||
fn is_last_subpage(&mut self) -> bool {
|
||||
self.current_subpage == self.subpages_in_current_page() - 1
|
||||
}
|
||||
|
||||
fn subpages_in_current_page(&mut self) -> usize {
|
||||
if self.is_xpub_page() {
|
||||
self.xpub_view.page_count()
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
/// Button layout for the current page.
|
||||
/// Normally there are arrows everywhere, apart from the right side of the
|
||||
/// last page. On xpub pages there is VIEW FULL middle button when it
|
||||
/// cannot fit one page. On xpub subpages there are wide arrows to
|
||||
/// scroll.
|
||||
fn get_button_layout(&mut self) -> ButtonLayout<T> {
|
||||
let (left, middle, right) = if self.is_in_subpage() {
|
||||
let left = Some(ButtonDetails::up_arrow_icon_wide());
|
||||
let right = if self.is_last_subpage() {
|
||||
None
|
||||
} else {
|
||||
Some(ButtonDetails::down_arrow_icon_wide())
|
||||
};
|
||||
(left, None, right)
|
||||
} else {
|
||||
let left = Some(ButtonDetails::left_arrow_icon());
|
||||
let middle = if self.is_xpub_page() && self.subpages_in_current_page() > 1 {
|
||||
Some(ButtonDetails::armed_text("VIEW FULL".into()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let right = if self.is_last_page() {
|
||||
None
|
||||
} else {
|
||||
Some(ButtonDetails::right_arrow_icon())
|
||||
};
|
||||
(left, middle, right)
|
||||
};
|
||||
ButtonLayout::new(left, middle, right)
|
||||
}
|
||||
|
||||
/// Reflecting the current page in the buttons.
|
||||
fn update_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
let btn_layout = self.get_button_layout();
|
||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||
buttons.set(btn_layout);
|
||||
});
|
||||
}
|
||||
|
||||
fn page_count(&self) -> usize {
|
||||
2 + self.xpubs.len()
|
||||
}
|
||||
|
||||
fn fill_xpub_page(&mut self, ctx: &mut EventCtx) {
|
||||
let i = self.current_page - 2;
|
||||
self.xpub_view.update_title(ctx, self.xpubs[i].0.clone());
|
||||
self.xpub_view.update_content(ctx, |p| {
|
||||
p.inner_mut().update(self.xpubs[i].1.clone());
|
||||
p.change_page(0)
|
||||
});
|
||||
}
|
||||
|
||||
fn change_page(&mut self, ctx: &mut EventCtx) {
|
||||
if self.is_xpub_page() {
|
||||
self.fill_xpub_page(ctx);
|
||||
}
|
||||
self.pad.clear();
|
||||
self.current_subpage = 0;
|
||||
}
|
||||
|
||||
fn change_subpage(&mut self, ctx: &mut EventCtx) {
|
||||
if self.is_xpub_page() {
|
||||
self.xpub_view
|
||||
.update_content(ctx, |p| p.change_page(self.current_subpage));
|
||||
self.pad.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for AddressDetails<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
type Msg = ();
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// QR code is being placed on the whole bounds, so it can be as big as possible
|
||||
// (it will not collide with the buttons, they are narrow and on the sides).
|
||||
// Therefore, also placing pad on the whole bounds.
|
||||
self.qr_code.place(bounds);
|
||||
self.pad.place(bounds);
|
||||
let (content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
self.details_view.place(content_area);
|
||||
self.xpub_view.place(content_area);
|
||||
self.buttons.place(button_area);
|
||||
self.area = content_area;
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Possibly update the components that have e.g. marquee
|
||||
match self.current_page {
|
||||
0 => self.qr_code.event(ctx, event),
|
||||
1 => self.details_view.event(ctx, event),
|
||||
_ => self.xpub_view.event(ctx, event),
|
||||
};
|
||||
|
||||
let button_event = self.buttons.event(ctx, event);
|
||||
if let Some(ButtonControllerMsg::Triggered(button)) = button_event {
|
||||
if self.is_in_subpage() {
|
||||
match button {
|
||||
ButtonPos::Left => {
|
||||
// Going back
|
||||
self.current_subpage -= 1;
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
// Going next
|
||||
self.current_subpage += 1;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.change_subpage(ctx);
|
||||
self.update_buttons(ctx);
|
||||
return None;
|
||||
} else {
|
||||
match button {
|
||||
ButtonPos::Left => {
|
||||
// Cancelling or going back
|
||||
if self.current_page == 0 {
|
||||
return Some(());
|
||||
}
|
||||
self.current_page -= 1;
|
||||
self.change_page(ctx);
|
||||
}
|
||||
ButtonPos::Right => {
|
||||
// Going to the next page
|
||||
self.current_page += 1;
|
||||
self.change_page(ctx);
|
||||
}
|
||||
ButtonPos::Middle => {
|
||||
// Going into subpage
|
||||
self.current_subpage = 1;
|
||||
self.change_subpage(ctx);
|
||||
}
|
||||
}
|
||||
self.update_buttons(ctx);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
self.buttons.paint();
|
||||
match self.current_page {
|
||||
0 => self.qr_code.paint(),
|
||||
1 => self.details_view.paint(),
|
||||
_ => self.xpub_view.paint(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_bounds")]
|
||||
fn bounds(&self, sink: &mut dyn FnMut(Rect)) {
|
||||
sink(self.area)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for AddressDetails<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("AddressDetails");
|
||||
match self.current_page {
|
||||
0 => t.child("qr_code", &self.qr_code),
|
||||
1 => t.child("details_view", &self.details_view),
|
||||
_ => t.child("xpub_view", &self.xpub_view),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,445 @@
|
||||
use super::{
|
||||
theme, Button, ButtonDetails, ButtonLayout, ButtonPos, HoldToConfirm, HoldToConfirmMsg,
|
||||
};
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
ui::{
|
||||
component::{base::Event, Component, EventCtx, Pad},
|
||||
event::{ButtonEvent, PhysicalButton},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
/// All possible states buttons (left and right) can be at.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
enum ButtonState {
|
||||
/// Both buttons are in untouched state.
|
||||
/// _ _
|
||||
/// NEXT: OneDown
|
||||
Nothing,
|
||||
/// One Button is down when previously nothing was.
|
||||
/// _ _ ... ↓ _ | _ ↓
|
||||
/// NEXT: Nothing, BothDown, HTCNeedsRelease
|
||||
OneDown(PhysicalButton),
|
||||
/// Both buttons are down ("middle-click").
|
||||
/// ↓ _ | _ ↓ ... ↓ ↓
|
||||
/// NEXT: OneReleased
|
||||
BothDown,
|
||||
/// One button is down when previously both were.
|
||||
/// Happens when "middle-click" is performed.
|
||||
/// ↓ ↓ ... ↓ _ | _ ↓
|
||||
/// NEXT: Nothing, BothDown
|
||||
OneReleased(PhysicalButton),
|
||||
/// One button is down after it triggered a HoldToConfirm event.
|
||||
/// Needed so that we can cleanly reset the state.
|
||||
/// ↓ _ | _ ↓ ... ↓ _ | _ ↓
|
||||
/// NEXT: Nothing
|
||||
HTCNeedsRelease(PhysicalButton),
|
||||
}
|
||||
|
||||
pub enum ButtonControllerMsg {
|
||||
Pressed(ButtonPos),
|
||||
Triggered(ButtonPos),
|
||||
}
|
||||
|
||||
/// Defines what kind of button should be currently used.
|
||||
pub enum ButtonType<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
Button(Button<T>),
|
||||
HoldToConfirm(HoldToConfirm<T>),
|
||||
Nothing,
|
||||
}
|
||||
|
||||
impl<T> ButtonType<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
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(HoldToConfirm::from_button_details(pos, btn_details))
|
||||
} else {
|
||||
Self::Button(Button::from_button_details(pos, btn_details))
|
||||
}
|
||||
} else {
|
||||
Self::Nothing
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
pos: ButtonPos,
|
||||
button_type: ButtonType<T>,
|
||||
}
|
||||
|
||||
impl<T> ButtonContainer<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger an action or end hold.
|
||||
/// Called when the button is released. If it is a simple button, it returns
|
||||
/// a Triggered message. If it is a hold-to-confirm button, it ends the
|
||||
/// hold.
|
||||
pub fn maybe_trigger(&mut self, ctx: &mut EventCtx) -> Option<ButtonControllerMsg> {
|
||||
match self.button_type {
|
||||
ButtonType::Button(_) => Some(ButtonControllerMsg::Triggered(self.pos)),
|
||||
_ => {
|
||||
self.hold_ended(ctx);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
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> ButtonController<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
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) {
|
||||
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> Component for ButtonController<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
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_event) => {
|
||||
let (new_state, event) = match self.state {
|
||||
// _ _
|
||||
ButtonState::Nothing => match button_event {
|
||||
// ▼ * | * ▼
|
||||
ButtonEvent::ButtonPressed(which) => (
|
||||
// ↓ _ | _ ↓
|
||||
ButtonState::OneDown(which),
|
||||
match which {
|
||||
// ▼ *
|
||||
PhysicalButton::Left => {
|
||||
self.left_btn.hold_started(ctx);
|
||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Left))
|
||||
}
|
||||
// * ▼
|
||||
PhysicalButton::Right => {
|
||||
self.right_btn.hold_started(ctx);
|
||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Right))
|
||||
}
|
||||
},
|
||||
),
|
||||
_ => (self.state, None),
|
||||
},
|
||||
// ↓ _ | _ ↓
|
||||
ButtonState::OneDown(which_down) => match button_event {
|
||||
// ▲ * | * ▲
|
||||
ButtonEvent::ButtonReleased(b) if b == which_down => match which_down {
|
||||
// ▲ *
|
||||
PhysicalButton::Left => {
|
||||
// _ _
|
||||
(ButtonState::Nothing, self.left_btn.maybe_trigger(ctx))
|
||||
}
|
||||
// * ▲
|
||||
PhysicalButton::Right => {
|
||||
// _ _
|
||||
(ButtonState::Nothing, self.right_btn.maybe_trigger(ctx))
|
||||
}
|
||||
},
|
||||
// * ▼ | ▼ *
|
||||
ButtonEvent::ButtonPressed(b) if b != which_down => {
|
||||
self.middle_hold_started(ctx);
|
||||
(
|
||||
// ↓ ↓
|
||||
ButtonState::BothDown,
|
||||
Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)),
|
||||
)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
// ↓ ↓
|
||||
ButtonState::BothDown => match button_event {
|
||||
// ▲ * | * ▲
|
||||
ButtonEvent::ButtonReleased(b) => {
|
||||
self.middle_btn.hold_ended(ctx);
|
||||
// _ ↓ | ↓ _
|
||||
(ButtonState::OneReleased(b), None)
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
// ↓ _ | _ ↓
|
||||
ButtonState::OneReleased(which_up) => match button_event {
|
||||
// * ▼ | ▼ *
|
||||
ButtonEvent::ButtonPressed(b) if b == which_up => {
|
||||
self.middle_hold_started(ctx);
|
||||
// ↓ ↓
|
||||
(ButtonState::BothDown, None)
|
||||
}
|
||||
// ▲ * | * ▲
|
||||
ButtonEvent::ButtonReleased(b) if b != which_up => {
|
||||
// _ _
|
||||
(ButtonState::Nothing, self.middle_btn.maybe_trigger(ctx))
|
||||
}
|
||||
_ => (self.state, None),
|
||||
},
|
||||
// ↓ _ | _ ↓
|
||||
ButtonState::HTCNeedsRelease(needs_release) => match button_event {
|
||||
// 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
|
||||
}
|
||||
// HoldToConfirm expiration
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T: StringType> crate::trace::Trace for ButtonContainer<T> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
if let ButtonType::Button(btn) = &self.button_type {
|
||||
btn.trace(t);
|
||||
} else if let ButtonType::HoldToConfirm(htc) = &self.button_type {
|
||||
htc.trace(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T: StringType> crate::trace::Trace for ButtonController<T> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ButtonController");
|
||||
t.child("left_btn", &self.left_btn);
|
||||
t.child("middle_btn", &self.middle_btn);
|
||||
t.child("right_btn", &self.right_btn);
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
use crate::ui::{
|
||||
component::{Component, Event, EventCtx, Never, Pad},
|
||||
display::Font,
|
||||
geometry::{Alignment, Point, Rect},
|
||||
util::long_line_content_with_ellipsis,
|
||||
};
|
||||
|
||||
use super::{common, 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> {
|
||||
pad: Pad,
|
||||
text: T,
|
||||
font: Font,
|
||||
/// Whether to show the text. Can be disabled.
|
||||
show_content: bool,
|
||||
alignment: Alignment,
|
||||
}
|
||||
|
||||
impl<T> ChangingTextLine<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
pub fn new(text: T, font: Font, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
pad: Pad::with_background(theme::BG),
|
||||
text,
|
||||
font,
|
||||
show_content: true,
|
||||
alignment,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_mono(text: T) -> Self {
|
||||
Self::new(text, Font::MONO, Alignment::Center)
|
||||
}
|
||||
|
||||
pub fn center_bold(text: T) -> Self {
|
||||
Self::new(text, Font::BOLD, Alignment::Center)
|
||||
}
|
||||
|
||||
/// Update the text to be displayed in the line.
|
||||
pub fn update_text(&mut self, text: T) {
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
/// Get current text.
|
||||
pub fn get_text(&self) -> &T {
|
||||
&self.text
|
||||
}
|
||||
|
||||
/// Whether we should display the text content.
|
||||
/// If not, the whole area (Pad) will still be cleared.
|
||||
/// Is valid until this function is called again.
|
||||
pub fn show_or_not(&mut self, show: bool) {
|
||||
self.show_content = show;
|
||||
}
|
||||
|
||||
/// 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.pad.area.y0 + self.font.line_height()
|
||||
}
|
||||
|
||||
/// Whether the whole text can be painted in the available space
|
||||
fn text_fits_completely(&self) -> bool {
|
||||
self.font.text_width(self.text.as_ref()) <= self.pad.area.width()
|
||||
}
|
||||
|
||||
fn paint_left(&self) {
|
||||
let baseline = Point::new(self.pad.area.x0, self.y_baseline());
|
||||
common::display(baseline, &self.text, self.font);
|
||||
}
|
||||
|
||||
fn paint_center(&self) {
|
||||
let baseline = Point::new(self.pad.area.bottom_center().x, self.y_baseline());
|
||||
common::display_center(baseline, &self.text, self.font);
|
||||
}
|
||||
|
||||
fn paint_right(&self) {
|
||||
let baseline = Point::new(self.pad.area.x1, self.y_baseline());
|
||||
common::display_right(baseline, &self.text, self.font);
|
||||
}
|
||||
|
||||
fn paint_long_content_with_ellipsis(&self) {
|
||||
let text_to_display = long_line_content_with_ellipsis(
|
||||
self.text.as_ref(),
|
||||
"...",
|
||||
self.font,
|
||||
self.pad.area.width(),
|
||||
);
|
||||
|
||||
// Creating the notion of motion by shifting the text left and right with
|
||||
// each new text character.
|
||||
// (So that it is apparent for the user that the text is changing.)
|
||||
let x_offset = if self.text.as_ref().len() % 2 == 0 {
|
||||
0
|
||||
} else {
|
||||
2
|
||||
};
|
||||
|
||||
let baseline = Point::new(self.pad.area.x0 + x_offset, self.y_baseline());
|
||||
common::display(baseline, &text_to_display, self.font);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for ChangingTextLine<T>
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.pad.place(bounds);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// Always re-painting from scratch.
|
||||
// Effectively clearing the line completely
|
||||
// when `self.show_content` is set to `false`.
|
||||
self.pad.clear();
|
||||
self.pad.paint();
|
||||
if self.show_content {
|
||||
// In the case text cannot fit, show ellipsis and its right part
|
||||
if !self.text_fits_completely() {
|
||||
self.paint_long_content_with_ellipsis();
|
||||
} else {
|
||||
match self.alignment {
|
||||
Alignment::Start => self.paint_left(),
|
||||
Alignment::Center => self.paint_center(),
|
||||
Alignment::End => self.paint_right(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
ui::{
|
||||
component::{
|
||||
base::Never, text::util::text_multiline_split_words, Component, Event, EventCtx,
|
||||
},
|
||||
display::Font,
|
||||
geometry::{Alignment, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
|
||||
const HEADER: &str = "COINJOIN IN PROGRESS";
|
||||
const FOOTER: &str = "Don't disconnect your Trezor";
|
||||
|
||||
pub struct CoinJoinProgress<T> {
|
||||
text: T,
|
||||
area: Rect,
|
||||
}
|
||||
|
||||
impl<T> CoinJoinProgress<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
pub fn new(text: T, _indeterminate: bool) -> Self {
|
||||
Self {
|
||||
text,
|
||||
area: Rect::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for CoinJoinProgress<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
type Msg = Never;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.area = bounds;
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option<Self::Msg> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// Trying to paint all three parts into the area, stopping if any of them
|
||||
// doesn't fit.
|
||||
let mut possible_rest = text_multiline_split_words(
|
||||
self.area,
|
||||
HEADER,
|
||||
Font::BOLD,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
Alignment::Center,
|
||||
);
|
||||
if let Some(rest) = possible_rest {
|
||||
possible_rest = text_multiline_split_words(
|
||||
rest,
|
||||
self.text.as_ref(),
|
||||
Font::MONO,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
Alignment::Center,
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if let Some(rest) = possible_rest {
|
||||
text_multiline_split_words(
|
||||
rest,
|
||||
FOOTER,
|
||||
Font::BOLD,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
Alignment::Center,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for CoinJoinProgress<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("CoinJoinProgress");
|
||||
t.string("header", HEADER);
|
||||
t.string("text", self.text.as_ref());
|
||||
t.string("footer", FOOTER);
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
use crate::ui::{
|
||||
display::{self, Font},
|
||||
geometry::Point,
|
||||
};
|
||||
|
||||
use super::theme;
|
||||
|
||||
/// Display white text on black background
|
||||
pub fn display<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_left(baseline, text.as_ref(), font, theme::FG, theme::BG);
|
||||
}
|
||||
|
||||
/// Display black text on white background
|
||||
pub fn display_inverse<T: AsRef<str>>(baseline: Point, text: &T, font: Font) {
|
||||
display::text_left(baseline, text.as_ref(), font, theme::BG, theme::FG);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
use crate::{
|
||||
time::Instant,
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
event::ButtonEvent,
|
||||
geometry::{Point, Rect},
|
||||
model_tr::component::{loader::Loader, ButtonPos, LoaderMsg, LoaderStyleSheet},
|
||||
},
|
||||
};
|
||||
|
||||
pub enum HoldToConfirmMsg {
|
||||
Confirmed,
|
||||
FailedToConfirm,
|
||||
}
|
||||
|
||||
pub struct HoldToConfirm {
|
||||
area: Rect,
|
||||
pos: ButtonPos,
|
||||
loader: Loader,
|
||||
baseline: Point,
|
||||
text_width: i16,
|
||||
}
|
||||
|
||||
impl HoldToConfirm {
|
||||
pub fn new(pos: ButtonPos, text: &'static str, styles: LoaderStyleSheet) -> Self {
|
||||
let text_width = styles.normal.font.text_width(text.as_ref());
|
||||
Self {
|
||||
area: Rect::zero(),
|
||||
pos,
|
||||
loader: Loader::new(text, styles),
|
||||
baseline: Point::zero(),
|
||||
text_width,
|
||||
}
|
||||
}
|
||||
|
||||
fn placement(&mut self, area: Rect, pos: ButtonPos) -> Rect {
|
||||
let button_width = self.text_width + 7;
|
||||
match pos {
|
||||
ButtonPos::Left => area.split_left(button_width).0,
|
||||
ButtonPos::Right => area.split_right(button_width).1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for HoldToConfirm {
|
||||
type Msg = HoldToConfirmMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let loader_area = self.placement(bounds, self.pos);
|
||||
self.loader.place(loader_area)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
|
||||
self.loader.start_growing(ctx, Instant::now());
|
||||
}
|
||||
Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => {
|
||||
if self.loader.is_animating() {
|
||||
self.loader.start_shrinking(ctx, Instant::now());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let msg = self.loader.event(ctx, event);
|
||||
|
||||
if let Some(LoaderMsg::GrownCompletely) = msg {
|
||||
return Some(HoldToConfirmMsg::Confirmed);
|
||||
}
|
||||
if let Some(LoaderMsg::ShrunkCompletely) = msg {
|
||||
return Some(HoldToConfirmMsg::FailedToConfirm);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.loader.paint();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl crate::trace::Trace for HoldToConfirm {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("HoldToConfirm");
|
||||
t.child("loader", &self.loader);
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
use super::button::{Button, ButtonMsg::Clicked};
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
display::Font,
|
||||
geometry::Rect,
|
||||
};
|
||||
|
||||
pub enum DialogMsg<T> {
|
||||
Content(T),
|
||||
LeftClicked,
|
||||
RightClicked,
|
||||
}
|
||||
|
||||
pub struct Dialog<T, U> {
|
||||
content: Child<T>,
|
||||
left_btn: Option<Child<Button<U>>>,
|
||||
right_btn: Option<Child<Button<U>>>,
|
||||
}
|
||||
|
||||
impl<T, U> Dialog<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
pub fn new(content: T, left: Option<Button<U>>, right: Option<Button<U>>) -> Self {
|
||||
Self {
|
||||
content: Child::new(content),
|
||||
left_btn: left.map(Child::new),
|
||||
right_btn: right.map(Child::new),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &T {
|
||||
self.content.inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Component for Dialog<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
type Msg = DialogMsg<T::Msg>;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let button_height = Font::BOLD.line_height() + 2;
|
||||
let (content_area, button_area) = bounds.split_bottom(button_height);
|
||||
self.content.place(content_area);
|
||||
self.left_btn.as_mut().map(|b| b.place(button_area));
|
||||
self.right_btn.as_mut().map(|b| b.place(button_area));
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
if let Some(msg) = self.content.event(ctx, event) {
|
||||
Some(DialogMsg::Content(msg))
|
||||
} else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||
Some(DialogMsg::LeftClicked)
|
||||
} else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||
Some(DialogMsg::RightClicked)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.content.paint();
|
||||
if let Some(b) = self.left_btn.as_mut() {
|
||||
b.paint();
|
||||
}
|
||||
if let Some(b) = self.right_btn.as_mut() {
|
||||
b.paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for Dialog<T, U>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
U: crate::trace::Trace + AsRef<str>,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Dialog");
|
||||
t.child("content", &self.content);
|
||||
if let Some(b) = self.left_btn.as_ref() {
|
||||
t.child("left", b)
|
||||
}
|
||||
if let Some(b) = self.right_btn.as_ref() {
|
||||
t.child("right", b)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,302 @@
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
ui::{
|
||||
component::{Child, Component, ComponentExt, Event, EventCtx, Pad, Paginate},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
scrollbar::SCROLLBAR_SPACE, theme, title::Title, ButtonAction, ButtonController,
|
||||
ButtonControllerMsg, ButtonLayout, ButtonPos, CancelInfoConfirmMsg, FlowPages, Page, ScrollBar,
|
||||
};
|
||||
|
||||
pub struct Flow<F, T>
|
||||
where
|
||||
F: Fn(usize) -> Page<T>,
|
||||
T: StringType + Clone,
|
||||
{
|
||||
/// Function to get pages from
|
||||
pages: FlowPages<F, T>,
|
||||
/// Instance of the current Page
|
||||
current_page: Page<T>,
|
||||
/// Title being shown at the top in bold
|
||||
title: Option<Title<T>>,
|
||||
scrollbar: Child<ScrollBar>,
|
||||
content_area: Rect,
|
||||
title_area: Rect,
|
||||
pad: Pad,
|
||||
buttons: Child<ButtonController<T>>,
|
||||
page_counter: usize,
|
||||
return_confirmed_index: bool,
|
||||
}
|
||||
|
||||
impl<F, T> Flow<F, T>
|
||||
where
|
||||
F: Fn(usize) -> Page<T>,
|
||||
T: StringType + Clone,
|
||||
{
|
||||
pub fn new(pages: FlowPages<F, T>) -> Self {
|
||||
let current_page = pages.get(0);
|
||||
Self {
|
||||
pages,
|
||||
current_page,
|
||||
title: None,
|
||||
content_area: Rect::zero(),
|
||||
title_area: Rect::zero(),
|
||||
scrollbar: Child::new(ScrollBar::to_be_filled_later()),
|
||||
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,
|
||||
return_confirmed_index: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: T) -> Self {
|
||||
self.title = Some(Title::new(title));
|
||||
self
|
||||
}
|
||||
|
||||
/// Causing the Flow to return the index of the page that was confirmed.
|
||||
pub fn with_return_confirmed_index(mut self) -> Self {
|
||||
self.return_confirmed_index = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn confirmed_index(&self) -> Option<usize> {
|
||||
self.return_confirmed_index.then_some(self.page_counter)
|
||||
}
|
||||
|
||||
/// Getting new current page according to page counter.
|
||||
/// Also updating the possible title and moving the scrollbar to correct
|
||||
/// position.
|
||||
fn change_current_page(&mut self, ctx: &mut EventCtx) {
|
||||
self.current_page = self.pages.get(self.page_counter);
|
||||
if self.title.is_some() {
|
||||
if let Some(title) = self.current_page.title() {
|
||||
self.title = Some(Title::new(title));
|
||||
self.title.place(self.title_area);
|
||||
}
|
||||
}
|
||||
let scrollbar_active_index = self
|
||||
.pages
|
||||
.scrollbar_page_index(self.content_area, self.page_counter);
|
||||
self.scrollbar.mutate(ctx, |_ctx, scrollbar| {
|
||||
scrollbar.change_page(scrollbar_active_index);
|
||||
});
|
||||
}
|
||||
|
||||
/// Placing current page, setting current buttons and clearing.
|
||||
fn update(&mut self, ctx: &mut EventCtx, get_new_page: bool) {
|
||||
if get_new_page {
|
||||
self.change_current_page(ctx);
|
||||
}
|
||||
self.current_page.place(self.content_area);
|
||||
self.set_buttons(ctx);
|
||||
self.scrollbar.request_complete_repaint(ctx);
|
||||
self.clear_and_repaint(ctx);
|
||||
}
|
||||
|
||||
/// Clearing the whole area and requesting repaint.
|
||||
fn clear_and_repaint(&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 the first page.
|
||||
fn go_to_first_page(&mut self, ctx: &mut EventCtx) {
|
||||
self.page_counter = 0;
|
||||
self.update(ctx, true);
|
||||
}
|
||||
|
||||
/// Going to the first page.
|
||||
fn go_to_last_page(&mut self, ctx: &mut EventCtx) {
|
||||
self.page_counter = self.pages.count() - 1;
|
||||
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 += jump as usize;
|
||||
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.
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/// Current choice is still the same, only its inner state has changed
|
||||
/// (its sub-page changed).
|
||||
fn update_after_current_choice_inner_change(&mut self, ctx: &mut EventCtx) {
|
||||
let inner_page = self.current_page.get_current_page();
|
||||
self.scrollbar.mutate(ctx, |ctx, scrollbar| {
|
||||
scrollbar.change_page(self.page_counter + inner_page);
|
||||
scrollbar.request_complete_repaint(ctx);
|
||||
});
|
||||
self.update(ctx, false);
|
||||
}
|
||||
|
||||
/// 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_after_current_choice_inner_change(ctx);
|
||||
true
|
||||
} else if matches!(pos, ButtonPos::Right) && self.current_page.has_next_page() {
|
||||
self.current_page.go_to_next_page();
|
||||
self.update_after_current_choice_inner_change(ctx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T> Component for Flow<F, T>
|
||||
where
|
||||
F: Fn(usize) -> Page<T>,
|
||||
T: StringType + Clone,
|
||||
{
|
||||
type Msg = CancelInfoConfirmMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let (title_content_area, button_area) = bounds.split_bottom(theme::BUTTON_HEIGHT);
|
||||
// Accounting for possible title
|
||||
let (title_area, content_area) = if self.title.is_some() {
|
||||
title_content_area.split_top(theme::FONT_HEADER.line_height())
|
||||
} else {
|
||||
(Rect::zero(), title_content_area)
|
||||
};
|
||||
self.content_area = content_area;
|
||||
|
||||
// Finding out the total amount of pages in this flow
|
||||
let complete_page_count = self.pages.scrollbar_page_count(content_area);
|
||||
// Redefining scrollbar now when we have its page_count
|
||||
self.scrollbar = Child::new(ScrollBar::new(complete_page_count));
|
||||
|
||||
// Placing a title and scrollbar in case the title is there
|
||||
// (scrollbar will be active - counting pages - even when not placed and
|
||||
// painted)
|
||||
if self.title.is_some() {
|
||||
let (title_area, scrollbar_area) =
|
||||
title_area.split_right(self.scrollbar.inner().overall_width() + SCROLLBAR_SPACE);
|
||||
|
||||
self.title.place(title_area);
|
||||
self.title_area = title_area;
|
||||
self.scrollbar.place(scrollbar_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> {
|
||||
ctx.set_page_count(self.pages.scrollbar_page_count(self.content_area));
|
||||
self.title.event(ctx, event);
|
||||
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::FirstPage => {
|
||||
self.go_to_first_page(ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::LastPage => {
|
||||
self.go_to_last_page(ctx);
|
||||
return None;
|
||||
}
|
||||
ButtonAction::Cancel => return Some(CancelInfoConfirmMsg::Cancelled),
|
||||
ButtonAction::Confirm => return Some(CancelInfoConfirmMsg::Confirmed),
|
||||
ButtonAction::Info => return Some(CancelInfoConfirmMsg::Info),
|
||||
}
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
// Scrollbars are painted only with a title
|
||||
if self.title.is_some() {
|
||||
self.scrollbar.paint();
|
||||
self.title.paint();
|
||||
}
|
||||
self.buttons.paint();
|
||||
// On purpose painting current page at the end, after buttons,
|
||||
// because we sometimes (in the case of QR code) need to use the
|
||||
// whole height of the display for showing the content
|
||||
// (and painting buttons last would cover the lower part).
|
||||
self.current_page.paint();
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F, T> crate::trace::Trace for Flow<F, T>
|
||||
where
|
||||
F: Fn(usize) -> Page<T>,
|
||||
T: StringType + Clone,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Flow");
|
||||
t.int("flow_page", self.page_counter as i64);
|
||||
t.int("flow_page_count", self.pages.count() as i64);
|
||||
|
||||
if let Some(title) = &self.title {
|
||||
t.child("title", title);
|
||||
}
|
||||
t.child("scrollbar", &self.scrollbar);
|
||||
t.child("buttons", &self.buttons);
|
||||
t.child("flow_page", &self.current_page);
|
||||
}
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
ui::{
|
||||
component::{base::Component, text::layout::LayoutFit, FormattedText, Paginate},
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{ButtonActions, ButtonDetails, ButtonLayout};
|
||||
|
||||
// So that there is only one implementation, and not multiple generic ones
|
||||
// as would be via `const N: usize` generics.
|
||||
const MAX_OPS_PER_PAGE: usize = 15;
|
||||
|
||||
/// 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, T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
F: Fn(usize) -> Page<T>,
|
||||
{
|
||||
/// Function/closure that will return appropriate page on demand.
|
||||
get_page: F,
|
||||
/// Number of pages in the flow.
|
||||
page_count: usize,
|
||||
}
|
||||
|
||||
impl<F, T> FlowPages<F, T>
|
||||
where
|
||||
F: Fn(usize) -> Page<T>,
|
||||
T: StringType + Clone,
|
||||
{
|
||||
pub fn new(get_page: F, page_count: usize) -> Self {
|
||||
Self {
|
||||
get_page,
|
||||
page_count,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a page on demand on a specified index.
|
||||
pub fn get(&self, page_index: usize) -> Page<T> {
|
||||
(self.get_page)(page_index)
|
||||
}
|
||||
|
||||
/// Total amount of pages.
|
||||
pub fn count(&self) -> usize {
|
||||
self.page_count
|
||||
}
|
||||
|
||||
/// How many scrollable pages are there in the flow
|
||||
/// (each page can have arbitrary number of "sub-pages").
|
||||
pub fn scrollbar_page_count(&self, bounds: Rect) -> usize {
|
||||
self.scrollbar_page_index(bounds, self.page_count)
|
||||
}
|
||||
|
||||
/// Active scrollbar position connected with the beginning of a specific
|
||||
/// page index.
|
||||
pub fn scrollbar_page_index(&self, bounds: Rect, page_index: usize) -> usize {
|
||||
let mut page_count = 0;
|
||||
for i in 0..page_index {
|
||||
let mut current_page = self.get(i);
|
||||
current_page.place(bounds);
|
||||
page_count += current_page.page_count;
|
||||
}
|
||||
page_count
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Page<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
formatted: FormattedText<T>,
|
||||
btn_layout: ButtonLayout<T>,
|
||||
btn_actions: ButtonActions,
|
||||
current_page: usize,
|
||||
page_count: usize,
|
||||
title: Option<T>,
|
||||
}
|
||||
|
||||
// For `layout.rs`
|
||||
impl<T> Page<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
pub fn new(
|
||||
btn_layout: ButtonLayout<T>,
|
||||
btn_actions: ButtonActions,
|
||||
formatted: FormattedText<T>,
|
||||
) -> Self {
|
||||
Self {
|
||||
formatted,
|
||||
btn_layout,
|
||||
btn_actions,
|
||||
current_page: 0,
|
||||
page_count: 1,
|
||||
title: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For `flow.rs`
|
||||
impl<T> Page<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
/// Adding title.
|
||||
pub fn with_title(mut self, title: T) -> Self {
|
||||
self.title = Some(title);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn paint(&mut self) {
|
||||
self.change_page(self.current_page);
|
||||
self.formatted.paint();
|
||||
}
|
||||
|
||||
pub fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.formatted.place(bounds);
|
||||
self.page_count = self.page_count();
|
||||
bounds
|
||||
}
|
||||
|
||||
pub fn btn_layout(&self) -> ButtonLayout<T> {
|
||||
// When we are in pagination inside this flow,
|
||||
// show the up and down arrows on appropriate sides.
|
||||
let current = self.btn_layout.clone();
|
||||
|
||||
// On the last page showing only the narrow arrow, so the right
|
||||
// button with possibly long text has enough space.
|
||||
let btn_left = if self.has_prev_page() && !self.has_next_page() {
|
||||
Some(ButtonDetails::up_arrow_icon())
|
||||
} else if self.has_prev_page() {
|
||||
Some(ButtonDetails::up_arrow_icon_wide())
|
||||
} else {
|
||||
current.btn_left
|
||||
};
|
||||
|
||||
// Middle button should be shown only on the last page, not to collide
|
||||
// with the fat right button.
|
||||
let (btn_middle, btn_right) = if self.has_next_page() {
|
||||
(None, Some(ButtonDetails::down_arrow_icon_wide()))
|
||||
} else {
|
||||
(current.btn_middle, current.btn_right)
|
||||
};
|
||||
|
||||
ButtonLayout::new(btn_left, btn_middle, btn_right)
|
||||
}
|
||||
|
||||
pub fn btn_actions(&self) -> ButtonActions {
|
||||
self.btn_actions
|
||||
}
|
||||
|
||||
pub fn title(&self) -> Option<T> {
|
||||
self.title.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;
|
||||
}
|
||||
|
||||
pub fn get_current_page(&self) -> usize {
|
||||
self.current_page
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
impl<T> Paginate for Page<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
fn page_count(&mut self) -> usize {
|
||||
self.formatted.page_count()
|
||||
}
|
||||
|
||||
fn change_page(&mut self, to_page: usize) {
|
||||
self.formatted.change_page(to_page)
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Page<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
use crate::ui::component::text::layout::trace::TraceSink;
|
||||
use core::cell::Cell;
|
||||
let fit: Cell<Option<LayoutFit>> = Cell::new(None);
|
||||
t.component("Page");
|
||||
if let Some(title) = &self.title {
|
||||
// Not calling it "title" as that is already traced by FlowPage
|
||||
t.string("page_title", title.as_ref());
|
||||
}
|
||||
t.int("active_page", self.current_page as i64);
|
||||
t.int("page_count", self.page_count as i64);
|
||||
t.in_list("text", &|l| {
|
||||
let result = self.formatted.layout_content_debug(&mut TraceSink(l));
|
||||
fit.set(Some(result));
|
||||
});
|
||||
}
|
||||
}
|
@ -1,78 +1,238 @@
|
||||
use super::theme;
|
||||
use crate::ui::{
|
||||
component::{Child, Component, Event, EventCtx},
|
||||
display::{self, Font},
|
||||
geometry::{Insets, Offset, Rect},
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
ui::{
|
||||
component::{Child, Component, ComponentExt, Event, EventCtx, Paginate},
|
||||
geometry::{Insets, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Frame<T, U> {
|
||||
area: Rect,
|
||||
title: U,
|
||||
use super::{super::constant, scrollbar::SCROLLBAR_SPACE, theme, title::Title, ScrollBar};
|
||||
|
||||
/// Component for holding another component and displaying a title.
|
||||
pub struct Frame<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: StringType,
|
||||
{
|
||||
title: Title<U>,
|
||||
content: Child<T>,
|
||||
}
|
||||
|
||||
impl<T, U> Frame<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<str>,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
pub fn new(title: U, content: T) -> Self {
|
||||
Self {
|
||||
title,
|
||||
area: Rect::zero(),
|
||||
title: Title::new(title),
|
||||
content: Child::new(content),
|
||||
}
|
||||
}
|
||||
|
||||
/// Aligning the title to the center, instead of the left.
|
||||
pub fn with_title_centered(mut self) -> Self {
|
||||
self.title = self.title.with_centered();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &T {
|
||||
self.content.inner()
|
||||
}
|
||||
|
||||
pub fn update_title(&mut self, ctx: &mut EventCtx, new_title: U) {
|
||||
self.title.set_text(ctx, new_title);
|
||||
}
|
||||
|
||||
pub fn update_content<F, R>(&mut self, ctx: &mut EventCtx, update_fn: F) -> R
|
||||
where
|
||||
F: Fn(&mut T) -> R,
|
||||
{
|
||||
self.content.mutate(ctx, |ctx, c| {
|
||||
let res = update_fn(c);
|
||||
c.request_complete_repaint(ctx);
|
||||
res
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Component for Frame<T, U>
|
||||
where
|
||||
T: Component,
|
||||
U: AsRef<str>,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
const TITLE_SPACE: i16 = 4;
|
||||
const TITLE_SPACE: i16 = 2;
|
||||
|
||||
let (title_area, content_area) = bounds.split_top(Font::BOLD.line_height());
|
||||
let (title_area, content_area) = bounds.split_top(theme::FONT_HEADER.line_height());
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
|
||||
self.area = title_area;
|
||||
self.title.place(title_area);
|
||||
self.content.place(content_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
self.title.event(ctx, event);
|
||||
self.content.event(ctx, event)
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
display::text_left(
|
||||
self.area.bottom_left() - Offset::y(2),
|
||||
self.title.as_ref(),
|
||||
Font::BOLD,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
display::dotted_line(self.area.bottom_left(), self.area.width(), theme::FG);
|
||||
self.title.paint();
|
||||
self.content.paint();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Paginate for Frame<T, U>
|
||||
where
|
||||
T: Component + Paginate,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
fn page_count(&mut self) -> usize {
|
||||
self.content.page_count()
|
||||
}
|
||||
|
||||
fn change_page(&mut self, active_page: usize) {
|
||||
self.content.change_page(active_page);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ScrollableContent {
|
||||
fn page_count(&self) -> usize;
|
||||
fn active_page(&self) -> usize;
|
||||
}
|
||||
|
||||
/// Component for holding another component and displaying a title.
|
||||
/// Also is allocating space for a scrollbar.
|
||||
pub struct ScrollableFrame<T, U>
|
||||
where
|
||||
T: Component + ScrollableContent,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
title: Option<Child<Title<U>>>,
|
||||
scrollbar: ScrollBar,
|
||||
content: Child<T>,
|
||||
}
|
||||
|
||||
impl<T, U> ScrollableFrame<T, U>
|
||||
where
|
||||
T: Component + ScrollableContent,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
pub fn new(content: T) -> Self {
|
||||
Self {
|
||||
title: None,
|
||||
scrollbar: ScrollBar::to_be_filled_later(),
|
||||
content: Child::new(content),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &T {
|
||||
self.content.inner()
|
||||
}
|
||||
|
||||
pub fn with_title(mut self, title: U) -> Self {
|
||||
self.title = Some(Child::new(Title::new(title)));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Component for ScrollableFrame<T, U>
|
||||
where
|
||||
T: Component + ScrollableContent,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
type Msg = T::Msg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
// Depending whether there is a title or not
|
||||
let (content_area, scrollbar_area, title_area) = if self.title.is_none() {
|
||||
// When the content fits on one page, no need for allocating place for scrollbar
|
||||
self.content.place(bounds);
|
||||
let page_count = self.content.inner().page_count();
|
||||
self.scrollbar.set_page_count(page_count);
|
||||
if page_count == 1 {
|
||||
(bounds, Rect::zero(), Rect::zero())
|
||||
} else {
|
||||
let (scrollbar_area, content_area) =
|
||||
bounds.split_top(ScrollBar::MAX_DOT_SIZE + constant::LINE_SPACE);
|
||||
(content_area, scrollbar_area, Rect::zero())
|
||||
}
|
||||
} else {
|
||||
const TITLE_SPACE: i16 = 2;
|
||||
|
||||
let (title_and_scrollbar_area, content_area) =
|
||||
bounds.split_top(theme::FONT_HEADER.line_height());
|
||||
let content_area = content_area.inset(Insets::top(TITLE_SPACE));
|
||||
|
||||
// When there is only one page, do not allocate anything for scrollbar,
|
||||
// which would reduce the space for title
|
||||
self.content.place(content_area);
|
||||
let page_count = self.content.inner().page_count();
|
||||
self.scrollbar.set_page_count(page_count);
|
||||
let (title_area, scrollbar_area) = if page_count == 1 {
|
||||
(title_and_scrollbar_area, Rect::zero())
|
||||
} else {
|
||||
title_and_scrollbar_area
|
||||
.split_right(self.scrollbar.overall_width() + SCROLLBAR_SPACE)
|
||||
};
|
||||
|
||||
(content_area, scrollbar_area, title_area)
|
||||
};
|
||||
|
||||
self.content.place(content_area);
|
||||
self.scrollbar.place(scrollbar_area);
|
||||
self.title.place(title_area);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
let msg = self.content.event(ctx, event);
|
||||
let content_active_page = self.content.inner().active_page();
|
||||
if self.scrollbar.active_page != content_active_page {
|
||||
self.scrollbar.change_page(content_active_page);
|
||||
self.scrollbar.request_complete_repaint(ctx);
|
||||
}
|
||||
self.title.event(ctx, event);
|
||||
msg
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.title.paint();
|
||||
self.scrollbar.paint();
|
||||
self.content.paint();
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for Frame<T, U>
|
||||
where
|
||||
T: crate::trace::Trace,
|
||||
U: AsRef<str>,
|
||||
T: crate::trace::Trace + Component,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Frame");
|
||||
t.string("title", self.title.as_ref());
|
||||
t.child("title", &self.title);
|
||||
t.child("content", &self.content);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T, U> crate::trace::Trace for ScrollableFrame<T, U>
|
||||
where
|
||||
T: crate::trace::Trace + Component + ScrollableContent,
|
||||
U: StringType + Clone,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ScrollableFrame");
|
||||
if let Some(title) = &self.title {
|
||||
t.child("title", title);
|
||||
}
|
||||
t.child("scrollbar", &self.scrollbar);
|
||||
t.child("content", &self.content);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,140 @@
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
time::{Duration, Instant},
|
||||
ui::{
|
||||
component::{Component, Event, EventCtx},
|
||||
event::ButtonEvent,
|
||||
geometry::Rect,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
loader::{Loader, DEFAULT_DURATION_MS},
|
||||
theme, ButtonContent, ButtonDetails, ButtonPos, LoaderMsg, LoaderStyleSheet,
|
||||
};
|
||||
|
||||
pub enum HoldToConfirmMsg {
|
||||
Confirmed,
|
||||
FailedToConfirm,
|
||||
}
|
||||
|
||||
pub struct HoldToConfirm<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
pos: ButtonPos,
|
||||
loader: Loader<T>,
|
||||
text_width: i16,
|
||||
}
|
||||
|
||||
impl<T> HoldToConfirm<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
pub fn text(pos: ButtonPos, text: T, styles: LoaderStyleSheet, duration: Duration) -> Self {
|
||||
let text_width = styles.normal.font.visible_text_width(text.as_ref());
|
||||
Self {
|
||||
pos,
|
||||
loader: Loader::text(text, styles).with_growing_duration(duration),
|
||||
text_width,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_button_details(pos: ButtonPos, btn_details: ButtonDetails<T>) -> Self {
|
||||
let duration = btn_details
|
||||
.duration
|
||||
.unwrap_or_else(|| Duration::from_millis(DEFAULT_DURATION_MS));
|
||||
match btn_details.content {
|
||||
ButtonContent::Text(text) => {
|
||||
Self::text(pos, text, LoaderStyleSheet::default_loader(), duration)
|
||||
}
|
||||
ButtonContent::Icon(_) => panic!("Icon is not supported"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updating the text of the component and re-placing it.
|
||||
pub fn set_text(&mut self, text: T, button_area: Rect) {
|
||||
self.text_width = self.loader.get_text_width(&text);
|
||||
self.loader.set_text(text);
|
||||
self.place(button_area);
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.loader.reset();
|
||||
}
|
||||
|
||||
pub fn set_duration(&mut self, duration: Duration) {
|
||||
self.loader.set_duration(duration);
|
||||
}
|
||||
|
||||
pub fn get_duration(&self) -> Duration {
|
||||
self.loader.get_duration()
|
||||
}
|
||||
|
||||
pub fn get_text(&self) -> &T {
|
||||
self.loader.get_text()
|
||||
}
|
||||
|
||||
fn placement(&mut self, area: Rect, pos: ButtonPos) -> Rect {
|
||||
let button_width = self.text_width + 2 * theme::BUTTON_OUTLINE;
|
||||
match pos {
|
||||
ButtonPos::Left => area.split_left(button_width).0,
|
||||
ButtonPos::Right => area.split_right(button_width).1,
|
||||
ButtonPos::Middle => area.split_center(button_width).1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for HoldToConfirm<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
type Msg = HoldToConfirmMsg;
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
let loader_area = self.placement(bounds, self.pos);
|
||||
self.loader.place(loader_area)
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
match event {
|
||||
Event::Button(ButtonEvent::HoldStarted) => {
|
||||
self.loader.start_growing(ctx, Instant::now());
|
||||
}
|
||||
Event::Button(ButtonEvent::HoldEnded) => {
|
||||
if self.loader.is_animating() {
|
||||
self.loader.start_shrinking(ctx, Instant::now());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let msg = self.loader.event(ctx, event);
|
||||
|
||||
if let Some(LoaderMsg::GrownCompletely) = msg {
|
||||
return Some(HoldToConfirmMsg::Confirmed);
|
||||
}
|
||||
if let Some(LoaderMsg::ShrunkCompletely) = msg {
|
||||
return Some(HoldToConfirmMsg::FailedToConfirm);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.loader.paint();
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for HoldToConfirm<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("HoldToConfirm");
|
||||
t.child("loader", &self.loader);
|
||||
}
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
trezorhal::usb::usb_configured,
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Label},
|
||||
display::{rect_fill, toif::Toif, Font},
|
||||
event::USBEvent,
|
||||
geometry::{self, Insets, Offset, Point, Rect},
|
||||
layout::util::get_user_custom_image,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::constant, common::display_center, theme, ButtonController, ButtonControllerMsg,
|
||||
ButtonLayout,
|
||||
};
|
||||
|
||||
const AREA: Rect = constant::screen();
|
||||
const TOP_CENTER: Point = AREA.top_center();
|
||||
const LABEL_Y: i16 = constant::HEIGHT - 15;
|
||||
const LABEL_AREA: Rect = AREA.split_top(LABEL_Y).1;
|
||||
const LOCKED_INSTRUCTION_Y: i16 = 27;
|
||||
const LOCKED_INSTRUCTION_AREA: Rect = AREA.split_top(LOCKED_INSTRUCTION_Y).1;
|
||||
const LOGO_ICON_TOP_MARGIN: i16 = 12;
|
||||
const LOCK_ICON_TOP_MARGIN: i16 = 12;
|
||||
const NOTIFICATION_HEIGHT: i16 = 12;
|
||||
const LABEL_OUTSET: i16 = 3;
|
||||
|
||||
pub struct Homescreen<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
// TODO label should be a Child in theory, but the homescreen image is not, so it is
|
||||
// always painted, so we need to always paint the label too
|
||||
label: Label<T>,
|
||||
notification: Option<(T, u8)>,
|
||||
/// Used for HTC functionality to lock device from homescreen
|
||||
invisible_buttons: Child<ButtonController<T>>,
|
||||
}
|
||||
|
||||
impl<T> Homescreen<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
pub fn new(label: T, notification: Option<(T, u8)>) -> Self {
|
||||
let invisible_btn_layout = ButtonLayout::htc_none_htc("".into(), "".into());
|
||||
Self {
|
||||
label: Label::centered(label, theme::TEXT_NORMAL),
|
||||
notification,
|
||||
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_homescreen_image(&self) {
|
||||
if let Ok(user_custom_image) = get_user_custom_image() {
|
||||
let toif_data = unwrap!(Toif::new(user_custom_image.as_ref()));
|
||||
toif_data.draw(TOP_CENTER, geometry::TOP_CENTER, theme::FG, theme::BG);
|
||||
} else {
|
||||
theme::ICON_LOGO.draw(
|
||||
TOP_CENTER + Offset::y(LOGO_ICON_TOP_MARGIN),
|
||||
geometry::TOP_CENTER,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_notification(&self) {
|
||||
let baseline = TOP_CENTER + Offset::y(Font::MONO.line_height());
|
||||
if !usb_configured() {
|
||||
self.fill_notification_background();
|
||||
// TODO: fill warning icons here as well?
|
||||
display_center(baseline, &"NO USB CONNECTION", Font::MONO);
|
||||
} else if let Some((notification, _level)) = &self.notification {
|
||||
self.fill_notification_background();
|
||||
// TODO: what if the notification text is so long it collides with icons?
|
||||
self.paint_warning_icons_in_top_corners();
|
||||
display_center(baseline, ¬ification.as_ref(), Font::MONO);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_label(&mut self) {
|
||||
// paint black background to place the label
|
||||
let mut outset = Insets::uniform(LABEL_OUTSET);
|
||||
// the margin at top is bigger (caused by text-height vs line-height?)
|
||||
// compensate by shrinking the outset
|
||||
outset.top -= 1;
|
||||
rect_fill(self.label.text_area().outset(outset), theme::BG);
|
||||
self.label.paint();
|
||||
}
|
||||
|
||||
/// So that notification is well visible even on homescreen image
|
||||
fn fill_notification_background(&self) {
|
||||
rect_fill(AREA.split_top(NOTIFICATION_HEIGHT).0, theme::BG);
|
||||
}
|
||||
|
||||
fn paint_warning_icons_in_top_corners(&self) {
|
||||
let warning_icon = theme::ICON_WARNING;
|
||||
warning_icon.draw(AREA.top_left(), geometry::TOP_LEFT, theme::FG, theme::BG);
|
||||
// Needs x+1 Offset to compensate for empty right column (icon needs to be
|
||||
// even-wide)
|
||||
warning_icon.draw(
|
||||
AREA.top_right() + Offset::x(1),
|
||||
geometry::TOP_RIGHT,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
}
|
||||
|
||||
fn event_usb(&mut self, ctx: &mut EventCtx, event: Event) {
|
||||
if let Event::USB(USBEvent::Connected(_)) = event {
|
||||
ctx.request_paint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for Homescreen<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
type Msg = ();
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.label.place(LABEL_AREA);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
Self::event_usb(self, ctx, event);
|
||||
// HTC press of any button will lock the device
|
||||
if let Some(ButtonControllerMsg::Triggered(_)) = self.invisible_buttons.event(ctx, event) {
|
||||
return Some(());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
// Painting the homescreen image first, as the notification and label
|
||||
// should be "on top of it"
|
||||
self.paint_homescreen_image();
|
||||
self.paint_notification();
|
||||
self.paint_label();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Lockscreen<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
label: Child<Label<T>>,
|
||||
instruction: Child<Label<T>>,
|
||||
/// Used for unlocking the device from lockscreen
|
||||
invisible_buttons: Child<ButtonController<T>>,
|
||||
}
|
||||
|
||||
impl<T> Lockscreen<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
pub fn new(label: T, bootscreen: bool) -> Self {
|
||||
let invisible_btn_layout = ButtonLayout::text_none_text("".into(), "".into());
|
||||
let instruction_str = if bootscreen {
|
||||
"Click to Connect"
|
||||
} else {
|
||||
"Click to Unlock"
|
||||
};
|
||||
Lockscreen {
|
||||
label: Child::new(Label::centered(label, theme::TEXT_NORMAL)),
|
||||
instruction: Child::new(Label::centered(instruction_str.into(), theme::TEXT_MONO)),
|
||||
invisible_buttons: Child::new(ButtonController::new(invisible_btn_layout)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Component for Lockscreen<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
type Msg = ();
|
||||
|
||||
fn place(&mut self, bounds: Rect) -> Rect {
|
||||
self.label.place(LABEL_AREA);
|
||||
self.instruction.place(LOCKED_INSTRUCTION_AREA);
|
||||
bounds
|
||||
}
|
||||
|
||||
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||
// Press of any button will unlock the device
|
||||
if let Some(ButtonControllerMsg::Triggered(_)) = self.invisible_buttons.event(ctx, event) {
|
||||
return Some(());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
theme::ICON_LOCK.draw(
|
||||
TOP_CENTER + Offset::y(LOCK_ICON_TOP_MARGIN),
|
||||
geometry::TOP_CENTER,
|
||||
theme::FG,
|
||||
theme::BG,
|
||||
);
|
||||
self.instruction.paint();
|
||||
self.label.paint();
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Homescreen<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Homescreen");
|
||||
t.child("label", &self.label);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T> crate::trace::Trace for Lockscreen<T>
|
||||
where
|
||||
T: StringType,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("Lockscreen");
|
||||
t.child("label", &self.label);
|
||||
}
|
||||
}
|
@ -0,0 +1,464 @@
|
||||
use crate::{
|
||||
strutil::StringType,
|
||||
ui::{
|
||||
component::{Child, Component, Event, EventCtx, Pad},
|
||||
geometry::{Offset, Rect},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{theme, ButtonController, ButtonControllerMsg, ButtonLayout, ButtonPos},
|
||||
choice_item::ChoiceItem,
|
||||
};
|
||||
|
||||
const DEFAULT_ITEMS_DISTANCE: i16 = 10;
|
||||
const DEFAULT_Y_BASELINE: i16 = 20;
|
||||
|
||||
pub trait Choice<T: StringType> {
|
||||
fn paint_center(&self, area: Rect, inverse: bool);
|
||||
fn width_center(&self) -> i16;
|
||||
|
||||
fn paint_side(&self, area: Rect);
|
||||
fn width_side(&self) -> i16;
|
||||
|
||||
fn btn_layout(&self) -> ButtonLayout<T> {
|
||||
ButtonLayout::default_three_icons()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// items in `heapless::Vec` (which caused StackOverflow),
|
||||
/// but offers a "lazy-loading" way of requesting the
|
||||
/// items only when they are needed, one-by-one.
|
||||
/// This way, no more than one item is stored in memory at any time.
|
||||
pub trait ChoiceFactory<T: StringType> {
|
||||
type Action;
|
||||
|
||||
fn count(&self) -> usize;
|
||||
fn get(&self, index: usize) -> (ChoiceItem<T>, Self::Action);
|
||||
}
|
||||
|
||||
/// 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 `Choice`s (through `ChoiceFactory`)
|
||||
/// and will receive back the index of the selected choice.
|
||||
///
|
||||
/// Each `Choice` 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, T, A>
|
||||
where
|
||||
F: ChoiceFactory<T, Action = A>,
|
||||
T: StringType,
|
||||
{
|
||||
choices: F,
|
||||
pad: Pad,
|
||||
buttons: Child<ButtonController<T>>,
|
||||
page_counter: usize,
|
||||
/// How many pixels from top should we render the items.
|
||||
y_baseline: i16,
|
||||
/// How many pixels are between the items.
|
||||
items_distance: i16,
|
||||
/// Whether the choice page is "infinite" (carousel).
|
||||
is_carousel: bool,
|
||||
/// Whether we should show items on left/right even when they cannot
|
||||
/// be painted entirely (they would be cut off).
|
||||
show_incomplete: bool,
|
||||
/// Whether to show only the currently selected item, nothing left/right.
|
||||
show_only_one_item: bool,
|
||||
/// Whether the middle selected item should be painted with
|
||||
/// inverse colors - black on white.
|
||||
inverse_selected_item: bool,
|
||||
}
|
||||
|
||||
impl<F, T, A> ChoicePage<F, T, A>
|
||||
where
|
||||
F: ChoiceFactory<T, Action = A>,
|
||||
T: StringType + Clone,
|
||||
{
|
||||
pub fn new(choices: F) -> Self {
|
||||
let initial_btn_layout = choices.get(0).0.btn_layout();
|
||||
|
||||
Self {
|
||||
choices,
|
||||
pad: Pad::with_background(theme::BG),
|
||||
buttons: Child::new(ButtonController::new(initial_btn_layout)),
|
||||
page_counter: 0,
|
||||
y_baseline: DEFAULT_Y_BASELINE,
|
||||
items_distance: DEFAULT_ITEMS_DISTANCE,
|
||||
is_carousel: false,
|
||||
show_incomplete: false,
|
||||
show_only_one_item: false,
|
||||
inverse_selected_item: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the page counter at the very beginning.
|
||||
/// Need to update the initial button layout.
|
||||
pub fn with_initial_page_counter(mut self, page_counter: usize) -> Self {
|
||||
self.page_counter = page_counter;
|
||||
let initial_btn_layout = self.get_current_choice().0.btn_layout();
|
||||
self.buttons = Child::new(ButtonController::new(initial_btn_layout));
|
||||
self
|
||||
}
|
||||
|
||||
/// Enabling the carousel mode.
|
||||
pub fn with_carousel(mut self, carousel: bool) -> Self {
|
||||
self.is_carousel = carousel;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show incomplete items, even when they cannot render in their entirety.
|
||||
pub fn with_incomplete(mut self, show_incomplete: bool) -> Self {
|
||||
self.show_incomplete = show_incomplete;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show only the currently selected item, nothing left/right.
|
||||
pub fn with_only_one_item(mut self, only_one_item: bool) -> Self {
|
||||
self.show_only_one_item = only_one_item;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adjust the horizontal baseline from the top of placement.
|
||||
pub fn with_y_baseline(mut self, y_baseline: i16) -> Self {
|
||||
self.y_baseline = y_baseline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adjust the distance between the items.
|
||||
pub fn with_items_distance(mut self, items_distance: i16) -> Self {
|
||||
self.items_distance = items_distance;
|
||||
self
|
||||
}
|
||||
|
||||
/// Resetting the component, which enables reusing the same instance
|
||||
/// for multiple choice categories.
|
||||
///
|
||||
/// Used for example in passphrase, where there are multiple categories of
|
||||
/// characters.
|
||||
pub fn reset(
|
||||
&mut self,
|
||||
ctx: &mut EventCtx,
|
||||
new_choices: F,
|
||||
new_page_counter: Option<usize>,
|
||||
is_carousel: bool,
|
||||
) {
|
||||
self.choices = new_choices;
|
||||
if let Some(new_counter) = new_page_counter {
|
||||
self.page_counter = new_counter;
|
||||
}
|
||||
self.is_carousel = is_carousel;
|
||||
self.update(ctx);
|
||||
}
|
||||
|
||||
/// Navigating to the chosen page index.
|
||||
pub fn set_page_counter(&mut self, ctx: &mut EventCtx, page_counter: usize) {
|
||||
self.page_counter = page_counter;
|
||||
self.update(ctx);
|
||||
}
|
||||
|
||||
/// Display current, previous and next choices according to
|
||||
/// the current ChoiceItem.
|
||||
fn paint_choices(&mut self) {
|
||||
let available_area = self.pad.area.split_top(self.y_baseline).0;
|
||||
|
||||
// Drawing the current item in the middle.
|
||||
self.show_current_choice(available_area);
|
||||
|
||||
// Not drawing the rest when not wanted
|
||||
if self.show_only_one_item {
|
||||
return;
|
||||
}
|
||||
|
||||
// Getting the remaining left and right areas.
|
||||
let center_width = self.get_current_choice().0.width_center();
|
||||
let (left_area, _center_area, right_area) = available_area.split_center(center_width);
|
||||
|
||||
// Possibly drawing on the left side.
|
||||
if self.has_previous_choice() || self.is_carousel {
|
||||
self.show_left_choices(left_area);
|
||||
}
|
||||
|
||||
// Possibly drawing on the right side.
|
||||
if self.has_next_choice() || self.is_carousel {
|
||||
self.show_right_choices(right_area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Setting current buttons, and clearing.
|
||||
fn update(&mut self, ctx: &mut EventCtx) {
|
||||
self.set_buttons(ctx);
|
||||
self.clear_and_repaint(ctx);
|
||||
}
|
||||
|
||||
/// Clearing the whole area and requesting repaint.
|
||||
fn clear_and_repaint(&mut self, ctx: &mut EventCtx) {
|
||||
self.pad.clear();
|
||||
ctx.request_paint();
|
||||
}
|
||||
|
||||
/// Index of the last page.
|
||||
fn last_page_index(&self) -> usize {
|
||||
self.choices.count() - 1
|
||||
}
|
||||
|
||||
/// Whether there is a previous choice (on the left).
|
||||
pub fn has_previous_choice(&self) -> bool {
|
||||
self.page_counter > 0
|
||||
}
|
||||
|
||||
/// Whether there is a next choice (on the right).
|
||||
pub fn has_next_choice(&self) -> bool {
|
||||
self.page_counter < self.last_page_index()
|
||||
}
|
||||
|
||||
/// Getting the choice on the current index
|
||||
pub fn get_current_choice(&self) -> (ChoiceItem<T>, A) {
|
||||
self.choices.get(self.page_counter)
|
||||
}
|
||||
|
||||
/// Display the current choice in the middle.
|
||||
fn show_current_choice(&mut self, area: Rect) {
|
||||
self.get_current_choice()
|
||||
.0
|
||||
.paint_center(area, self.inverse_selected_item);
|
||||
|
||||
// Color inversion is just one-time thing.
|
||||
if self.inverse_selected_item {
|
||||
self.inverse_selected_item = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Display all the choices fitting on the left side.
|
||||
/// Going as far as possible.
|
||||
fn show_left_choices(&self, area: Rect) {
|
||||
// NOTE: page index can get negative here, so having it as i16 instead of usize
|
||||
let mut page_index = self.page_counter as i16 - 1;
|
||||
let mut current_area = area.split_right(self.items_distance).0;
|
||||
while current_area.width() > 0 {
|
||||
// Breaking out of the loop if we exhausted left items
|
||||
// and the carousel mode is not enabled.
|
||||
if page_index < 0 {
|
||||
if self.is_carousel {
|
||||
// Moving to the last page.
|
||||
page_index = self.last_page_index() as i16;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (choice, _) = self.choices.get(page_index as usize);
|
||||
let choice_width = choice.width_side();
|
||||
|
||||
if current_area.width() <= choice_width && !self.show_incomplete {
|
||||
// early break for an item that will not fit the remaining space
|
||||
break;
|
||||
}
|
||||
|
||||
// We need to calculate the area explicitly because we want to allow it
|
||||
// to exceed the bounds of the original area.
|
||||
let choice_area = Rect::from_top_right_and_size(
|
||||
current_area.top_right(),
|
||||
Offset::new(choice_width, current_area.height()),
|
||||
);
|
||||
choice.paint_side(choice_area);
|
||||
|
||||
// Updating loop variables.
|
||||
current_area = current_area
|
||||
.split_right(choice_width + self.items_distance)
|
||||
.0;
|
||||
page_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Display all the choices fitting on the right side.
|
||||
/// Going as far as possible.
|
||||
fn show_right_choices(&self, area: Rect) {
|
||||
let mut page_index = self.page_counter + 1;
|
||||
// start with a little offset to account for the middle highlight
|
||||
let mut current_area = area.split_left(self.items_distance + 3).1;
|
||||
while current_area.width() > 0 {
|
||||
// Breaking out of the loop if we exhausted right items
|
||||
// and the carousel mode is not enabled.
|
||||
if page_index > self.last_page_index() {
|
||||
if self.is_carousel {
|
||||
// Moving to the first page.
|
||||
page_index = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (choice, _) = self.choices.get(page_index);
|
||||
let choice_width = choice.width_side();
|
||||
|
||||
if current_area.width() <= choice_width && !self.show_incomplete {
|
||||
// early break for an item that will not fit the remaining space
|
||||
break;
|
||||
}
|
||||
|
||||
// We need to calculate the area explicitly because we want to allow it
|
||||
// to exceed the bounds of the original area.
|
||||
let choice_area = Rect::from_top_left_and_size(
|
||||
current_area.top_left(),
|
||||
Offset::new(choice_width, current_area.height()),
|
||||
);
|
||||
choice.paint_side(choice_area);
|
||||
|
||||
// Updating loop variables.
|
||||
current_area = current_area
|
||||
.split_left(choice_width + self.items_distance)
|
||||
.1;
|
||||
page_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrease the page counter to the previous page.
|
||||
fn decrease_page_counter(&mut self) {
|
||||
self.page_counter -= 1;
|
||||
}
|
||||
|
||||
/// Advance page counter to the next page.
|
||||
fn increase_page_counter(&mut self) {
|
||||
self.page_counter += 1;
|
||||
}
|
||||
|
||||
/// Set page to the first one.
|
||||
fn page_counter_to_zero(&mut self) {
|
||||
self.page_counter = 0;
|
||||
}
|
||||
|
||||
/// Set page to the last one.
|
||||
fn page_counter_to_max(&mut self) {
|
||||
self.page_counter = self.last_page_index();
|
||||
}
|
||||
|
||||
/// Get current page counter.
|
||||
pub fn page_index(&self) -> usize {
|
||||
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.
|
||||
fn set_buttons(&mut self, ctx: &mut EventCtx) {
|
||||
let btn_layout = self.get_current_choice().0.btn_layout();
|
||||
self.buttons.mutate(ctx, |_ctx, buttons| {
|
||||
buttons.set(btn_layout);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn choice_factory(&self) -> &F {
|
||||
&self.choices
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, T, A> Component for ChoicePage<F, T, A>
|
||||
where
|
||||
F: ChoiceFactory<T, Action = A>,
|
||||
T: StringType + Clone,
|
||||
{
|
||||
type Msg = A;
|
||||
|
||||
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);
|
||||
|
||||
// Button was "triggered" - released. Doing the appropriate action.
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
ButtonPos::Middle => {
|
||||
// Clicked SELECT. Send current choice index
|
||||
self.clear_and_repaint(ctx);
|
||||
return Some(self.get_current_choice().1);
|
||||
}
|
||||
}
|
||||
};
|
||||
// The middle button was "pressed", highlighting the current choice by color
|
||||
// inversion.
|
||||
if let Some(ButtonControllerMsg::Pressed(ButtonPos::Middle)) = button_event {
|
||||
self.inverse_selected_item = true;
|
||||
self.clear_and_repaint(ctx);
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn paint(&mut self) {
|
||||
self.pad.paint();
|
||||
self.buttons.paint();
|
||||
self.paint_choices();
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<F, T, A> crate::trace::Trace for ChoicePage<F, T, A>
|
||||
where
|
||||
F: ChoiceFactory<T, Action = A>,
|
||||
T: StringType + Clone,
|
||||
{
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ChoicePage");
|
||||
t.int("active_page", self.page_counter as i64);
|
||||
t.int("page_count", self.choices.count() as i64);
|
||||
t.bool("is_carousel", self.is_carousel);
|
||||
|
||||
if self.has_previous_choice() {
|
||||
t.child("prev_choice", &self.choices.get(self.page_counter - 1).0);
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the left end.
|
||||
t.child("prev_choice", &self.choices.get(self.last_page_index()).0);
|
||||
}
|
||||
|
||||
t.child("current_choice", &self.choices.get(self.page_counter).0);
|
||||
|
||||
if self.has_next_choice() {
|
||||
t.child("next_choice", &self.choices.get(self.page_counter + 1).0);
|
||||
} else if self.is_carousel {
|
||||
// In case of carousel going to the very left.
|
||||
t.child("next_choice", &self.choices.get(0).0);
|
||||
}
|
||||
|
||||
t.child("buttons", &self.buttons);
|
||||
}
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
use crate::{
|
||||
strutil::{ShortString, StringType},
|
||||
ui::{
|
||||
display::{self, rect_fill, rect_fill_corners, rect_outline_rounded, Font, Icon},
|
||||
geometry::{Offset, Rect, BOTTOM_LEFT},
|
||||
},
|
||||
};
|
||||
|
||||
use heapless::String;
|
||||
|
||||
use super::super::{theme, ButtonDetails, ButtonLayout, Choice};
|
||||
|
||||
const ICON_RIGHT_PADDING: i16 = 2;
|
||||
|
||||
/// Simple string component used as a choice item.
|
||||
#[derive(Clone)]
|
||||
pub struct ChoiceItem<T: StringType> {
|
||||
text: ShortString,
|
||||
icon: Option<Icon>,
|
||||
btn_layout: ButtonLayout<T>,
|
||||
font: Font,
|
||||
}
|
||||
|
||||
impl<T: StringType> ChoiceItem<T> {
|
||||
pub fn new<U: AsRef<str>>(text: U, btn_layout: ButtonLayout<T>) -> Self {
|
||||
Self {
|
||||
text: String::from(text.as_ref()),
|
||||
icon: None,
|
||||
btn_layout,
|
||||
font: theme::FONT_CHOICE_ITEMS,
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows to add the icon.
|
||||
pub fn with_icon(mut self, icon: Icon) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
/// Allows to change the font.
|
||||
pub fn with_font(mut self, font: Font) -> Self {
|
||||
self.font = font;
|
||||
self
|
||||
}
|
||||
|
||||
/// Setting left button.
|
||||
pub fn set_left_btn(&mut self, btn_left: Option<ButtonDetails<T>>) {
|
||||
self.btn_layout.btn_left = btn_left;
|
||||
}
|
||||
|
||||
/// Setting middle button.
|
||||
pub fn set_middle_btn(&mut self, btn_middle: Option<ButtonDetails<T>>) {
|
||||
self.btn_layout.btn_middle = btn_middle;
|
||||
}
|
||||
|
||||
/// Setting right button.
|
||||
pub fn set_right_btn(&mut self, btn_right: Option<ButtonDetails<T>>) {
|
||||
self.btn_layout.btn_right = btn_right;
|
||||
}
|
||||
|
||||
/// Changing the text.
|
||||
pub fn set_text(&mut self, text: ShortString) {
|
||||
self.text = text;
|
||||
}
|
||||
|
||||
fn side_text(&self) -> Option<&str> {
|
||||
if self.icon.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(self.text.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content(&self) -> &str {
|
||||
self.text.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Choice<T> for ChoiceItem<T>
|
||||
where
|
||||
T: StringType + Clone,
|
||||
{
|
||||
/// Painting the item as the main choice in the middle.
|
||||
/// Showing both the icon and text, if the icon is available.
|
||||
fn paint_center(&self, area: Rect, inverse: bool) {
|
||||
let width = text_icon_width(Some(self.text.as_ref()), self.icon, self.font);
|
||||
paint_rounded_highlight(area, Offset::new(width, self.font.text_height()), inverse);
|
||||
paint_text_icon(
|
||||
area,
|
||||
width,
|
||||
Some(self.text.as_ref()),
|
||||
self.icon,
|
||||
self.font,
|
||||
inverse,
|
||||
);
|
||||
}
|
||||
|
||||
/// Getting the overall width in pixels when displayed in center.
|
||||
/// That means both the icon and text will be shown.
|
||||
fn width_center(&self) -> i16 {
|
||||
text_icon_width(Some(self.text.as_ref()), self.icon, self.font)
|
||||
}
|
||||
|
||||
/// Getting the non-central width in pixels.
|
||||
/// It will show an icon if defined, otherwise the text, not both.
|
||||
fn width_side(&self) -> i16 {
|
||||
text_icon_width(self.side_text(), self.icon, self.font)
|
||||
}
|
||||
|
||||
/// Painting smaller version of the item on the side.
|
||||
fn paint_side(&self, area: Rect) {
|
||||
let width = text_icon_width(self.side_text(), self.icon, self.font);
|
||||
paint_text_icon(area, width, self.side_text(), self.icon, self.font, false);
|
||||
}
|
||||
|
||||
/// Getting current button layout.
|
||||
fn btn_layout(&self) -> ButtonLayout<T> {
|
||||
self.btn_layout.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_rounded_highlight(area: Rect, size: Offset, inverse: bool) {
|
||||
let bound = theme::BUTTON_OUTLINE;
|
||||
let left_bottom = area.bottom_center() + Offset::new(-size.x / 2 - bound, bound + 1);
|
||||
let x_size = size.x + 2 * bound;
|
||||
let y_size = size.y + 2 * bound;
|
||||
let outline_size = Offset::new(x_size, y_size);
|
||||
let outline = Rect::from_bottom_left_and_size(left_bottom, outline_size);
|
||||
if inverse {
|
||||
rect_fill(outline, theme::FG);
|
||||
rect_fill_corners(outline, theme::BG);
|
||||
} else {
|
||||
rect_outline_rounded(outline, theme::FG, theme::BG, 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn text_icon_width(text: Option<&str>, icon: Option<Icon>, font: Font) -> i16 {
|
||||
match (text, icon) {
|
||||
(Some(text), Some(icon)) => {
|
||||
icon.toif.width() + ICON_RIGHT_PADDING + font.visible_text_width(text)
|
||||
}
|
||||
(Some(text), None) => font.visible_text_width(text),
|
||||
(None, Some(icon)) => icon.toif.width(),
|
||||
(None, None) => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_text_icon(
|
||||
area: Rect,
|
||||
width: i16,
|
||||
text: Option<&str>,
|
||||
icon: Option<Icon>,
|
||||
font: Font,
|
||||
inverse: bool,
|
||||
) {
|
||||
let fg_color = if inverse { theme::BG } else { theme::FG };
|
||||
let bg_color = if inverse { theme::FG } else { theme::BG };
|
||||
|
||||
let mut baseline = area.bottom_center() - Offset::x(width / 2);
|
||||
if let Some(icon) = icon {
|
||||
let height_diff = font.text_height() - icon.toif.height();
|
||||
let vertical_offset = Offset::y(-height_diff / 2);
|
||||
icon.draw(baseline + vertical_offset, BOTTOM_LEFT, fg_color, bg_color);
|
||||
baseline = baseline + Offset::x(icon.toif.width() + ICON_RIGHT_PADDING);
|
||||
}
|
||||
|
||||
if let Some(text) = text {
|
||||
// Possibly shifting the baseline left, when there is a text bearing.
|
||||
// This is to center the text properly.
|
||||
baseline = baseline - Offset::x(font.start_x_bearing(text));
|
||||
display::text_left(baseline, text, font, fg_color, bg_color);
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG-ONLY SECTION BELOW
|
||||
|
||||
#[cfg(feature = "ui_debug")]
|
||||
impl<T: StringType> crate::trace::Trace for ChoiceItem<T> {
|
||||
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||
t.component("ChoiceItem");
|
||||
t.string("content", self.text.as_ref());
|
||||
}
|
||||
}
|