diff options
Diffstat (limited to 'src/ui/text.c')
-rw-r--r-- | src/ui/text.c | 277 |
1 files changed, 10 insertions, 267 deletions
diff --git a/src/ui/text.c b/src/ui/text.c index edbc6583..c851be90 100644 --- a/src/ui/text.c +++ b/src/ui/text.c | |||
@@ -46,6 +46,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | |||
46 | #include <SDL_version.h> | 46 | #include <SDL_version.h> |
47 | #include <stdarg.h> | 47 | #include <stdarg.h> |
48 | 48 | ||
49 | #if defined (LAGRANGE_ENABLE_HARFBUZZ) | ||
50 | # include <hb.h> | ||
51 | #endif | ||
52 | |||
49 | #if SDL_VERSION_ATLEAST(2, 0, 10) | 53 | #if SDL_VERSION_ATLEAST(2, 0, 10) |
50 | # define LAGRANGE_RASTER_DEPTH 8 | 54 | # define LAGRANGE_RASTER_DEPTH 8 |
51 | # define LAGRANGE_RASTER_FORMAT SDL_PIXELFORMAT_INDEX8 | 55 | # define LAGRANGE_RASTER_FORMAT SDL_PIXELFORMAT_INDEX8 |
@@ -909,278 +913,17 @@ struct Impl_RunArgs { | |||
909 | int * runAdvance_out; | 913 | int * runAdvance_out; |
910 | }; | 914 | }; |
911 | 915 | ||
916 | #if defined (LAGRANGE_ENABLE_HARFBUZZ) | ||
912 | static iRect run_Font_(iFont *d, const iRunArgs *args) { | 917 | static iRect run_Font_(iFont *d, const iRunArgs *args) { |
913 | iRect bounds = zero_Rect(); | 918 | iRect bounds = zero_Rect(); |
914 | const iInt2 orig = args->pos; | 919 | const iInt2 orig = args->pos; |
915 | float xpos = orig.x; | ||
916 | float xposMax = xpos; | ||
917 | float monoAdvance = 0; | ||
918 | int ypos = orig.y; | ||
919 | size_t maxLen = args->maxLen ? args->maxLen : iInvalidSize; | ||
920 | float xposExtend = orig.x; /* allows wide glyphs to use more space; restored by whitespace */ | ||
921 | const enum iRunMode mode = args->mode; | ||
922 | const char * lastWordEnd = args->text.start; | ||
923 | iAssert(args->xposLimit == 0 || isMeasuring_(mode)); | ||
924 | iAssert(args->text.end >= args->text.start); | ||
925 | if (args->continueFrom_out) { | ||
926 | *args->continueFrom_out = args->text.end; | ||
927 | } | ||
928 | iChar prevCh = 0; | ||
929 | const iBool isMonospaced = d->isMonospaced && !(mode & alwaysVariableWidthFlag_RunMode); | ||
930 | if (isMonospaced) { | ||
931 | monoAdvance = glyph_Font_(d, 'M')->advance; | ||
932 | } | ||
933 | if (args->mode & fillBackground_RunMode) { | ||
934 | const iColor initial = get_Color(args->color); | ||
935 | SDL_SetRenderDrawColor(text_.render, initial.r, initial.g, initial.b, 0); | ||
936 | } | ||
937 | /* Text rendering is not very straightforward! Let's dive in... */ | ||
938 | for (const char *chPos = args->text.start; chPos != args->text.end; ) { | ||
939 | iAssert(chPos < args->text.end); | ||
940 | const char *currentPos = chPos; | ||
941 | if (*chPos == 0x1b) { /* ANSI escape. */ | ||
942 | chPos++; | ||
943 | iRegExpMatch m; | ||
944 | init_RegExpMatch(&m); | ||
945 | if (match_RegExp(text_.ansiEscape, chPos, args->text.end - chPos, &m)) { | ||
946 | if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { | ||
947 | /* Change the color. */ | ||
948 | const iColor clr = | ||
949 | ansiForeground_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId); | ||
950 | SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); | ||
951 | if (args->mode & fillBackground_RunMode) { | ||
952 | SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); | ||
953 | } | ||
954 | } | ||
955 | chPos = end_RegExpMatch(&m); | ||
956 | continue; | ||
957 | } | ||
958 | } | ||
959 | iChar ch = nextChar_(&chPos, args->text.end); | ||
960 | iBool isEmoji = isEmoji_Char(ch); | ||
961 | if (ch == 0x200d) { /* zero-width joiner */ | ||
962 | /* We don't have the composited Emojis. */ | ||
963 | if (isEmoji_Char(prevCh)) { | ||
964 | /* skip */ | ||
965 | nextChar_(&chPos, args->text.end); | ||
966 | ch = nextChar_(&chPos, args->text.end); | ||
967 | } | ||
968 | } | ||
969 | if (isVariationSelector_Char(ch)) { | ||
970 | ch = nextChar_(&chPos, args->text.end); /* skip it */ | ||
971 | } | ||
972 | /* Special instructions. */ { | ||
973 | if (ch == 0xad) { /* soft hyphen */ | ||
974 | lastWordEnd = chPos; | ||
975 | if (isMeasuring_(mode)) { | ||
976 | if (args->xposLimit > 0) { | ||
977 | const char *postHyphen = chPos; | ||
978 | iChar nextCh = nextChar_(&postHyphen, args->text.end); | ||
979 | if ((int) xpos + glyph_Font_(d, ch)->rect[0].size.x + | ||
980 | glyph_Font_(d, nextCh)->rect[0].size.x > args->xposLimit) { | ||
981 | /* Wraps after hyphen, should show it. */ | ||
982 | } | ||
983 | else continue; | ||
984 | } | ||
985 | else continue; | ||
986 | } | ||
987 | else { | ||
988 | /* Only show it at the end. */ | ||
989 | if (chPos != args->text.end) { | ||
990 | continue; | ||
991 | } | ||
992 | } | ||
993 | } | ||
994 | /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */ | ||
995 | if (ch == '\n') { | ||
996 | if (args->xposLimit > 0 && mode & stopAtNewline_RunMode) { | ||
997 | /* Stop the line here, this is a hard warp. */ | ||
998 | if (args->continueFrom_out) { | ||
999 | *args->continueFrom_out = chPos; | ||
1000 | } | ||
1001 | break; | ||
1002 | } | ||
1003 | xpos = xposExtend = orig.x; | ||
1004 | ypos += d->height; | ||
1005 | prevCh = ch; | ||
1006 | continue; | ||
1007 | } | ||
1008 | if (ch == '\t') { | ||
1009 | const int tabStopWidth = d->height * 10; | ||
1010 | const int halfWidth = (iMax(args->xposLimit, args->xposLayoutBound) - orig.x) / 2; | ||
1011 | const int xRel = xpos - orig.x; | ||
1012 | /* First stop is always to half width. */ | ||
1013 | if (halfWidth > 0 && xRel < halfWidth) { | ||
1014 | xpos = orig.x + halfWidth; | ||
1015 | } | ||
1016 | else if (halfWidth > 0 && xRel < halfWidth * 3 / 2) { | ||
1017 | xpos = orig.x + halfWidth * 3 / 2; | ||
1018 | } | ||
1019 | else { | ||
1020 | xpos = orig.x + ((xRel / tabStopWidth) + 1) * tabStopWidth; | ||
1021 | } | ||
1022 | xposExtend = iMax(xposExtend, xpos); | ||
1023 | prevCh = 0; | ||
1024 | continue; | ||
1025 | } | ||
1026 | if (ch == '\v') { /* color change */ | ||
1027 | iChar esc = nextChar_(&chPos, args->text.end); | ||
1028 | int colorNum = args->color; | ||
1029 | if (esc == '\v') { /* Extended range. */ | ||
1030 | esc = nextChar_(&chPos, args->text.end) + asciiExtended_ColorEscape; | ||
1031 | colorNum = esc - asciiBase_ColorEscape; | ||
1032 | } | ||
1033 | else if (esc != 0x24) { /* ASCII Cancel */ | ||
1034 | colorNum = esc - asciiBase_ColorEscape; | ||
1035 | } | ||
1036 | if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { | ||
1037 | const iColor clr = get_Color(colorNum); | ||
1038 | SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); | ||
1039 | if (args->mode & fillBackground_RunMode) { | ||
1040 | SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); | ||
1041 | } | ||
1042 | } | ||
1043 | prevCh = 0; | ||
1044 | continue; | ||
1045 | } | ||
1046 | if (isDefaultIgnorable_Char(ch) || isFitzpatrickType_Char(ch)) { | ||
1047 | continue; | ||
1048 | } | ||
1049 | } | ||
1050 | const iGlyph *glyph = glyph_Font_(d, ch); | ||
1051 | int x1 = iMax(xpos, xposExtend); | ||
1052 | /* Which half of the pixel the glyph falls on? */ | ||
1053 | const int hoff = enableHalfPixelGlyphs_Text ? (xpos - x1 > 0.5f ? 1 : 0) : 0; | ||
1054 | if (mode & draw_RunMode && ch != 0x20 && ch != 0 && !isRasterized_Glyph_(glyph, hoff)) { | ||
1055 | /* Need to pause here and make sure all glyphs have been cached in the text. */ | ||
1056 | // printf("[Text] missing from cache: %lc (%x)\n", (int) ch, ch); | ||
1057 | cacheTextGlyphs_Font_(d, args->text); | ||
1058 | glyph = glyph_Font_(d, ch); /* cache may have been reset */ | ||
1059 | } | ||
1060 | int x2 = x1 + glyph->rect[hoff].size.x; | ||
1061 | /* Out of the allotted space on the line? */ | ||
1062 | if (args->xposLimit > 0 && x2 > args->xposLimit) { | ||
1063 | if (args->continueFrom_out) { | ||
1064 | if (lastWordEnd != args->text.start && ~mode & noWrapFlag_RunMode) { | ||
1065 | *args->continueFrom_out = skipSpace_CStr(lastWordEnd); | ||
1066 | *args->continueFrom_out = iMin(*args->continueFrom_out, | ||
1067 | args->text.end); | ||
1068 | } | ||
1069 | else { | ||
1070 | *args->continueFrom_out = currentPos; /* forced break */ | ||
1071 | } | ||
1072 | } | ||
1073 | break; | ||
1074 | } | ||
1075 | const int yLineMax = ypos + d->height; | ||
1076 | SDL_Rect dst = { x1 + glyph->d[hoff].x, | ||
1077 | ypos + glyph->font->baseline + glyph->d[hoff].y, | ||
1078 | glyph->rect[hoff].size.x, | ||
1079 | glyph->rect[hoff].size.y }; | ||
1080 | if (glyph->font != d) { | ||
1081 | if (glyph->font->height > d->height) { | ||
1082 | /* Center-align vertically so the baseline isn't totally offset. */ | ||
1083 | dst.y -= (glyph->font->height - d->height) / 2; | ||
1084 | } | ||
1085 | } | ||
1086 | /* Update the bounding box. */ | ||
1087 | if (mode & visualFlag_RunMode) { | ||
1088 | if (isEmpty_Rect(bounds)) { | ||
1089 | bounds = init_Rect(dst.x, dst.y, dst.w, dst.h); | ||
1090 | } | ||
1091 | else { | ||
1092 | bounds = union_Rect(bounds, init_Rect(dst.x, dst.y, dst.w, dst.h)); | ||
1093 | } | ||
1094 | } | ||
1095 | else { | ||
1096 | bounds.size.x = iMax(bounds.size.x, x2 - orig.x); | ||
1097 | bounds.size.y = iMax(bounds.size.y, ypos + glyph->font->height - orig.y); | ||
1098 | } | ||
1099 | /* Symbols and emojis are NOT monospaced, so must conform when the primary font | ||
1100 | is monospaced. Except with Japanese script, that's larger than the normal monospace. */ | ||
1101 | const iBool useMonoAdvance = | ||
1102 | monoAdvance > 0 && !isJapanese_FontId(fontId_Text_(glyph->font)); | ||
1103 | const float advance = (useMonoAdvance && glyph->advance > 0 ? monoAdvance : glyph->advance); | ||
1104 | if (!isMeasuring_(mode) && ch != 0x20 /* don't bother rendering spaces */) { | ||
1105 | if (useMonoAdvance && dst.w > advance && glyph->font != d && !isEmoji) { | ||
1106 | /* Glyphs from a different font may need recentering to look better. */ | ||
1107 | dst.x -= (dst.w - advance) / 2; | ||
1108 | } | ||
1109 | SDL_Rect src; | ||
1110 | memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect)); | ||
1111 | /* Clip the glyphs to the font's height. This is useful when the font's line spacing | ||
1112 | has been reduced or when the glyph is from a different font. */ | ||
1113 | if (dst.y + dst.h > yLineMax) { | ||
1114 | const int over = dst.y + dst.h - yLineMax; | ||
1115 | src.h -= over; | ||
1116 | dst.h -= over; | ||
1117 | } | ||
1118 | if (dst.y < ypos) { | ||
1119 | const int over = ypos - dst.y; | ||
1120 | dst.y += over; | ||
1121 | dst.h -= over; | ||
1122 | src.y += over; | ||
1123 | src.h -= over; | ||
1124 | } | ||
1125 | if (args->mode & fillBackground_RunMode) { | ||
1126 | /* Alpha blending looks much better if the RGB components don't change in | ||
1127 | the partially transparent pixels. */ | ||
1128 | SDL_RenderFillRect(text_.render, &dst); | ||
1129 | } | ||
1130 | SDL_RenderCopy(text_.render, text_.cache, &src, &dst); | ||
1131 | } | ||
1132 | xpos += advance; | ||
1133 | if (!isSpace_Char(ch)) { | ||
1134 | xposExtend += isEmoji ? glyph->advance : advance; | ||
1135 | } | ||
1136 | #if defined (LAGRANGE_ENABLE_KERNING) | ||
1137 | /* Check the next character. */ | ||
1138 | if (!isMonospaced && glyph->font == d) { | ||
1139 | /* TODO: No need to decode the next char twice; check this on the next iteration. */ | ||
1140 | const char *peek = chPos; | ||
1141 | const iChar next = nextChar_(&peek, args->text.end); | ||
1142 | if (enableKerning_Text && !d->manualKernOnly && next) { | ||
1143 | const uint32_t nextGlyphIndex = glyphIndex_Font_(glyph->font, next); | ||
1144 | int kern = stbtt_GetGlyphKernAdvance( | ||
1145 | &glyph->font->font, glyph->glyphIndex, nextGlyphIndex); | ||
1146 | /* Nunito needs some kerning fixes. */ | ||
1147 | if (glyph->font->family == nunito_TextFont) { | ||
1148 | if (ch == 'W' && (next == 'i' || next == 'h')) { | ||
1149 | kern = -30; | ||
1150 | } | ||
1151 | else if (ch == 'T' && next == 'h') { | ||
1152 | kern = -15; | ||
1153 | } | ||
1154 | else if (ch == 'V' && next == 'i') { | ||
1155 | kern = -15; | ||
1156 | } | ||
1157 | } | ||
1158 | if (kern) { | ||
1159 | // printf("%lc(%u) -> %lc(%u): kern %d (%f)\n", ch, glyph->glyphIndex, next, | ||
1160 | // nextGlyphIndex, | ||
1161 | // kern, d->xScale * kern); | ||
1162 | xpos += glyph->font->xScale * kern; | ||
1163 | xposExtend += glyph->font->xScale * kern; | ||
1164 | } | ||
1165 | } | ||
1166 | } | ||
1167 | #endif | ||
1168 | xposExtend = iMax(xposExtend, xpos); | ||
1169 | xposMax = iMax(xposMax, xposExtend); | ||
1170 | if (args->continueFrom_out && ((mode & noWrapFlag_RunMode) || isWrapBoundary_(prevCh, ch))) { | ||
1171 | lastWordEnd = currentPos; /* mark word wrap position */ | ||
1172 | } | ||
1173 | prevCh = ch; | ||
1174 | if (--maxLen == 0) { | ||
1175 | break; | ||
1176 | } | ||
1177 | } | ||
1178 | if (args->runAdvance_out) { | ||
1179 | *args->runAdvance_out = xposMax - orig.x; | ||
1180 | } | ||
1181 | fflush(stdout); | ||
1182 | return bounds; | 920 | return bounds; |
1183 | } | 921 | } |
922 | #else /* !defined (LAGRANGE_ENABLE_HARFBUZZ) */ | ||
923 | /* The fallback method: an incomplete solution for simple scripts. */ | ||
924 | # define run_Font_ runSimple_Font_ | ||
925 | # include "text_simple.c" | ||
926 | #endif | ||
1184 | 927 | ||
1185 | int lineHeight_Text(int fontId) { | 928 | int lineHeight_Text(int fontId) { |
1186 | return font_Text_(fontId)->height; | 929 | return font_Text_(fontId)->height; |