diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-06-26 07:02:46 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-06-26 07:02:46 +0300 |
commit | 512702de740c3c6b38097d101f1f454d7da22e97 (patch) | |
tree | f0562a6872131fcc271d4838d3761a67408d62e7 | |
parent | 5dbc85eaaa1bd0a0fc11dd76a75ece2efe763df5 (diff) |
Text: Link with HarfBuzz; old run_Font_ is a fallback
HarfBuzz will provide proper Unicode text shaping for both simple and complex scripts.
The old `run_Font_` is available for use as a fallback if HarfBuzz is not available due to size or complexity constraints (it's written in C++).
-rw-r--r-- | CMakeLists.txt | 5 | ||||
-rw-r--r-- | Depends.cmake | 12 | ||||
-rw-r--r-- | src/ui/text.c | 277 | ||||
-rw-r--r-- | src/ui/text_simple.c | 300 |
4 files changed, 327 insertions, 267 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 47cd2442..e6baba76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt | |||
@@ -29,6 +29,7 @@ if (IOS) | |||
29 | endif () | 29 | endif () |
30 | 30 | ||
31 | # Build configuration. | 31 | # Build configuration. |
32 | option (ENABLE_HARFBUZZ "Use HarfBuzz to shape text" ON) | ||
32 | option (ENABLE_IPC "Use IPC to communicate between running instances" ON) | 33 | option (ENABLE_IPC "Use IPC to communicate between running instances" ON) |
33 | option (ENABLE_MPG123 "Use mpg123 for decoding MPEG audio" ON) | 34 | option (ENABLE_MPG123 "Use mpg123 for decoding MPEG audio" ON) |
34 | option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF) | 35 | option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF) |
@@ -308,6 +309,10 @@ if (ENABLE_CUSTOM_FRAME AND MSYS) | |||
308 | endif () | 309 | endif () |
309 | target_link_libraries (app PUBLIC the_Foundation::the_Foundation) | 310 | target_link_libraries (app PUBLIC the_Foundation::the_Foundation) |
310 | target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) | 311 | target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) |
312 | if (ENABLE_HARFBUZZ AND HARFBUZZ_FOUND) | ||
313 | target_link_libraries (app PUBLIC harfbuzz) | ||
314 | target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_HARFBUZZ=1) | ||
315 | endif () | ||
311 | if (APPLE) | 316 | if (APPLE) |
312 | if (IOS) | 317 | if (IOS) |
313 | target_link_libraries (app PUBLIC "-framework UIKit") | 318 | target_link_libraries (app PUBLIC "-framework UIKit") |
diff --git a/Depends.cmake b/Depends.cmake index 7507ebc6..f543f576 100644 --- a/Depends.cmake +++ b/Depends.cmake | |||
@@ -3,6 +3,18 @@ if (IOS) | |||
3 | return () | 3 | return () |
4 | endif () | 4 | endif () |
5 | 5 | ||
6 | if (ENABLE_HARFBUZZ AND EXISTS ${CMAKE_SOURCE_DIR}/lib/harfbuzz) | ||
7 | # Build HarfBuzz with minimal dependencies. | ||
8 | set (HB_BUILD_SUBSET OFF CACHE BOOL "" FORCE) | ||
9 | set (HB_HAVE_CORETEXT OFF CACHE BOOL "" FORCE) | ||
10 | set (HB_HAVE_FREETYPE OFF CACHE BOOL "" FORCE) | ||
11 | set (HB_HAVE_GLIB OFF CACHE BOOL "" FORCE) | ||
12 | set (HB_HAVE_GOBJECT OFF CACHE BOOL "" FORCE) | ||
13 | set (HB_HAVE_ICU OFF CACHE BOOL "" FORCE) | ||
14 | add_subdirectory (${CMAKE_SOURCE_DIR}/lib/harfbuzz) | ||
15 | set (HARFBUZZ_FOUND YES) | ||
16 | endif () | ||
17 | |||
6 | if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt) | 18 | if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt) |
7 | set (INSTALL_THE_FOUNDATION YES) | 19 | set (INSTALL_THE_FOUNDATION YES) |
8 | find_package (the_Foundation REQUIRED) | 20 | find_package (the_Foundation REQUIRED) |
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; |
diff --git a/src/ui/text_simple.c b/src/ui/text_simple.c new file mode 100644 index 00000000..baa87e4b --- /dev/null +++ b/src/ui/text_simple.c | |||
@@ -0,0 +1,300 @@ | |||
1 | /* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi> | ||
2 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. Redistributions in binary form must reproduce the above copyright notice, | ||
9 | this list of conditions and the following disclaimer in the documentation | ||
10 | and/or other materials provided with the distribution. | ||
11 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
20 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | /* this file is included from text.c, so it doesn't use includes of its own */ | ||
24 | |||
25 | static iRect runSimple_Font_(iFont *d, const iRunArgs *args) { | ||
26 | /* This function shapes text using a simplified, incomplete algorithm. It works for English | ||
27 | and other simple LTR scripts. Composed glyphs are not supported (must rely on text being | ||
28 | in a pre-composed form). This algorithm is used if HarfBuzz is not available. */ | ||
29 | iRect bounds = zero_Rect(); | ||
30 | const iInt2 orig = args->pos; | ||
31 | float xpos = orig.x; | ||
32 | float xposMax = xpos; | ||
33 | float monoAdvance = 0; | ||
34 | int ypos = orig.y; | ||
35 | size_t maxLen = args->maxLen ? args->maxLen : iInvalidSize; | ||
36 | float xposExtend = orig.x; /* allows wide glyphs to use more space; restored by whitespace */ | ||
37 | const enum iRunMode mode = args->mode; | ||
38 | const char * lastWordEnd = args->text.start; | ||
39 | iAssert(args->xposLimit == 0 || isMeasuring_(mode)); | ||
40 | iAssert(args->text.end >= args->text.start); | ||
41 | if (args->continueFrom_out) { | ||
42 | *args->continueFrom_out = args->text.end; | ||
43 | } | ||
44 | iChar prevCh = 0; | ||
45 | const iBool isMonospaced = d->isMonospaced && !(mode & alwaysVariableWidthFlag_RunMode); | ||
46 | if (isMonospaced) { | ||
47 | monoAdvance = glyph_Font_(d, 'M')->advance; | ||
48 | } | ||
49 | if (args->mode & fillBackground_RunMode) { | ||
50 | const iColor initial = get_Color(args->color); | ||
51 | SDL_SetRenderDrawColor(text_.render, initial.r, initial.g, initial.b, 0); | ||
52 | } | ||
53 | /* Text rendering is not very straightforward! Let's dive in... */ | ||
54 | for (const char *chPos = args->text.start; chPos != args->text.end; ) { | ||
55 | iAssert(chPos < args->text.end); | ||
56 | const char *currentPos = chPos; | ||
57 | if (*chPos == 0x1b) { /* ANSI escape. */ | ||
58 | chPos++; | ||
59 | iRegExpMatch m; | ||
60 | init_RegExpMatch(&m); | ||
61 | if (match_RegExp(text_.ansiEscape, chPos, args->text.end - chPos, &m)) { | ||
62 | if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { | ||
63 | /* Change the color. */ | ||
64 | const iColor clr = | ||
65 | ansiForeground_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId); | ||
66 | SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); | ||
67 | if (args->mode & fillBackground_RunMode) { | ||
68 | SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); | ||
69 | } | ||
70 | } | ||
71 | chPos = end_RegExpMatch(&m); | ||
72 | continue; | ||
73 | } | ||
74 | } | ||
75 | iChar ch = nextChar_(&chPos, args->text.end); | ||
76 | iBool isEmoji = isEmoji_Char(ch); | ||
77 | if (ch == 0x200d) { /* zero-width joiner */ | ||
78 | /* We don't have the composited Emojis. */ | ||
79 | if (isEmoji_Char(prevCh)) { | ||
80 | /* skip */ | ||
81 | nextChar_(&chPos, args->text.end); | ||
82 | ch = nextChar_(&chPos, args->text.end); | ||
83 | } | ||
84 | } | ||
85 | if (isVariationSelector_Char(ch)) { | ||
86 | ch = nextChar_(&chPos, args->text.end); /* skip it */ | ||
87 | } | ||
88 | /* Special instructions. */ { | ||
89 | if (ch == 0xad) { /* soft hyphen */ | ||
90 | lastWordEnd = chPos; | ||
91 | if (isMeasuring_(mode)) { | ||
92 | if (args->xposLimit > 0) { | ||
93 | const char *postHyphen = chPos; | ||
94 | iChar nextCh = nextChar_(&postHyphen, args->text.end); | ||
95 | if ((int) xpos + glyph_Font_(d, ch)->rect[0].size.x + | ||
96 | glyph_Font_(d, nextCh)->rect[0].size.x > args->xposLimit) { | ||
97 | /* Wraps after hyphen, should show it. */ | ||
98 | } | ||
99 | else continue; | ||
100 | } | ||
101 | else continue; | ||
102 | } | ||
103 | else { | ||
104 | /* Only show it at the end. */ | ||
105 | if (chPos != args->text.end) { | ||
106 | continue; | ||
107 | } | ||
108 | } | ||
109 | } | ||
110 | /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */ | ||
111 | if (ch == '\n') { | ||
112 | if (args->xposLimit > 0 && mode & stopAtNewline_RunMode) { | ||
113 | /* Stop the line here, this is a hard warp. */ | ||
114 | if (args->continueFrom_out) { | ||
115 | *args->continueFrom_out = chPos; | ||
116 | } | ||
117 | break; | ||
118 | } | ||
119 | xpos = xposExtend = orig.x; | ||
120 | ypos += d->height; | ||
121 | prevCh = ch; | ||
122 | continue; | ||
123 | } | ||
124 | if (ch == '\t') { | ||
125 | const int tabStopWidth = d->height * 10; | ||
126 | const int halfWidth = (iMax(args->xposLimit, args->xposLayoutBound) - orig.x) / 2; | ||
127 | const int xRel = xpos - orig.x; | ||
128 | /* First stop is always to half width. */ | ||
129 | if (halfWidth > 0 && xRel < halfWidth) { | ||
130 | xpos = orig.x + halfWidth; | ||
131 | } | ||
132 | else if (halfWidth > 0 && xRel < halfWidth * 3 / 2) { | ||
133 | xpos = orig.x + halfWidth * 3 / 2; | ||
134 | } | ||
135 | else { | ||
136 | xpos = orig.x + ((xRel / tabStopWidth) + 1) * tabStopWidth; | ||
137 | } | ||
138 | xposExtend = iMax(xposExtend, xpos); | ||
139 | prevCh = 0; | ||
140 | continue; | ||
141 | } | ||
142 | if (ch == '\v') { /* color change */ | ||
143 | iChar esc = nextChar_(&chPos, args->text.end); | ||
144 | int colorNum = args->color; | ||
145 | if (esc == '\v') { /* Extended range. */ | ||
146 | esc = nextChar_(&chPos, args->text.end) + asciiExtended_ColorEscape; | ||
147 | colorNum = esc - asciiBase_ColorEscape; | ||
148 | } | ||
149 | else if (esc != 0x24) { /* ASCII Cancel */ | ||
150 | colorNum = esc - asciiBase_ColorEscape; | ||
151 | } | ||
152 | if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { | ||
153 | const iColor clr = get_Color(colorNum); | ||
154 | SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); | ||
155 | if (args->mode & fillBackground_RunMode) { | ||
156 | SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); | ||
157 | } | ||
158 | } | ||
159 | prevCh = 0; | ||
160 | continue; | ||
161 | } | ||
162 | if (isDefaultIgnorable_Char(ch) || isFitzpatrickType_Char(ch)) { | ||
163 | continue; | ||
164 | } | ||
165 | } | ||
166 | const iGlyph *glyph = glyph_Font_(d, ch); | ||
167 | int x1 = iMax(xpos, xposExtend); | ||
168 | /* Which half of the pixel the glyph falls on? */ | ||
169 | const int hoff = enableHalfPixelGlyphs_Text ? (xpos - x1 > 0.5f ? 1 : 0) : 0; | ||
170 | if (mode & draw_RunMode && ch != 0x20 && ch != 0 && !isRasterized_Glyph_(glyph, hoff)) { | ||
171 | /* Need to pause here and make sure all glyphs have been cached in the text. */ | ||
172 | // printf("[Text] missing from cache: %lc (%x)\n", (int) ch, ch); | ||
173 | cacheTextGlyphs_Font_(d, args->text); | ||
174 | glyph = glyph_Font_(d, ch); /* cache may have been reset */ | ||
175 | } | ||
176 | int x2 = x1 + glyph->rect[hoff].size.x; | ||
177 | /* Out of the allotted space on the line? */ | ||
178 | if (args->xposLimit > 0 && x2 > args->xposLimit) { | ||
179 | if (args->continueFrom_out) { | ||
180 | if (lastWordEnd != args->text.start && ~mode & noWrapFlag_RunMode) { | ||
181 | *args->continueFrom_out = skipSpace_CStr(lastWordEnd); | ||
182 | *args->continueFrom_out = iMin(*args->continueFrom_out, | ||
183 | args->text.end); | ||
184 | } | ||
185 | else { | ||
186 | *args->continueFrom_out = currentPos; /* forced break */ | ||
187 | } | ||
188 | } | ||
189 | break; | ||
190 | } | ||
191 | const int yLineMax = ypos + d->height; | ||
192 | SDL_Rect dst = { x1 + glyph->d[hoff].x, | ||
193 | ypos + glyph->font->baseline + glyph->d[hoff].y, | ||
194 | glyph->rect[hoff].size.x, | ||
195 | glyph->rect[hoff].size.y }; | ||
196 | if (glyph->font != d) { | ||
197 | if (glyph->font->height > d->height) { | ||
198 | /* Center-align vertically so the baseline isn't totally offset. */ | ||
199 | dst.y -= (glyph->font->height - d->height) / 2; | ||
200 | } | ||
201 | } | ||
202 | /* Update the bounding box. */ | ||
203 | if (mode & visualFlag_RunMode) { | ||
204 | if (isEmpty_Rect(bounds)) { | ||
205 | bounds = init_Rect(dst.x, dst.y, dst.w, dst.h); | ||
206 | } | ||
207 | else { | ||
208 | bounds = union_Rect(bounds, init_Rect(dst.x, dst.y, dst.w, dst.h)); | ||
209 | } | ||
210 | } | ||
211 | else { | ||
212 | bounds.size.x = iMax(bounds.size.x, x2 - orig.x); | ||
213 | bounds.size.y = iMax(bounds.size.y, ypos + glyph->font->height - orig.y); | ||
214 | } | ||
215 | /* Symbols and emojis are NOT monospaced, so must conform when the primary font | ||
216 | is monospaced. Except with Japanese script, that's larger than the normal monospace. */ | ||
217 | const iBool useMonoAdvance = | ||
218 | monoAdvance > 0 && !isJapanese_FontId(fontId_Text_(glyph->font)); | ||
219 | const float advance = (useMonoAdvance && glyph->advance > 0 ? monoAdvance : glyph->advance); | ||
220 | if (!isMeasuring_(mode) && ch != 0x20 /* don't bother rendering spaces */) { | ||
221 | if (useMonoAdvance && dst.w > advance && glyph->font != d && !isEmoji) { | ||
222 | /* Glyphs from a different font may need recentering to look better. */ | ||
223 | dst.x -= (dst.w - advance) / 2; | ||
224 | } | ||
225 | SDL_Rect src; | ||
226 | memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect)); | ||
227 | /* Clip the glyphs to the font's height. This is useful when the font's line spacing | ||
228 | has been reduced or when the glyph is from a different font. */ | ||
229 | if (dst.y + dst.h > yLineMax) { | ||
230 | const int over = dst.y + dst.h - yLineMax; | ||
231 | src.h -= over; | ||
232 | dst.h -= over; | ||
233 | } | ||
234 | if (dst.y < ypos) { | ||
235 | const int over = ypos - dst.y; | ||
236 | dst.y += over; | ||
237 | dst.h -= over; | ||
238 | src.y += over; | ||
239 | src.h -= over; | ||
240 | } | ||
241 | if (args->mode & fillBackground_RunMode) { | ||
242 | /* Alpha blending looks much better if the RGB components don't change in | ||
243 | the partially transparent pixels. */ | ||
244 | SDL_RenderFillRect(text_.render, &dst); | ||
245 | } | ||
246 | SDL_RenderCopy(text_.render, text_.cache, &src, &dst); | ||
247 | } | ||
248 | xpos += advance; | ||
249 | if (!isSpace_Char(ch)) { | ||
250 | xposExtend += isEmoji ? glyph->advance : advance; | ||
251 | } | ||
252 | #if defined (LAGRANGE_ENABLE_KERNING) | ||
253 | /* Check the next character. */ | ||
254 | if (!isMonospaced && glyph->font == d) { | ||
255 | /* TODO: No need to decode the next char twice; check this on the next iteration. */ | ||
256 | const char *peek = chPos; | ||
257 | const iChar next = nextChar_(&peek, args->text.end); | ||
258 | if (enableKerning_Text && !d->manualKernOnly && next) { | ||
259 | const uint32_t nextGlyphIndex = glyphIndex_Font_(glyph->font, next); | ||
260 | int kern = stbtt_GetGlyphKernAdvance( | ||
261 | &glyph->font->font, glyph->glyphIndex, nextGlyphIndex); | ||
262 | /* Nunito needs some kerning fixes. */ | ||
263 | if (glyph->font->family == nunito_TextFont) { | ||
264 | if (ch == 'W' && (next == 'i' || next == 'h')) { | ||
265 | kern = -30; | ||
266 | } | ||
267 | else if (ch == 'T' && next == 'h') { | ||
268 | kern = -15; | ||
269 | } | ||
270 | else if (ch == 'V' && next == 'i') { | ||
271 | kern = -15; | ||
272 | } | ||
273 | } | ||
274 | if (kern) { | ||
275 | // printf("%lc(%u) -> %lc(%u): kern %d (%f)\n", ch, glyph->glyphIndex, next, | ||
276 | // nextGlyphIndex, | ||
277 | // kern, d->xScale * kern); | ||
278 | xpos += glyph->font->xScale * kern; | ||
279 | xposExtend += glyph->font->xScale * kern; | ||
280 | } | ||
281 | } | ||
282 | } | ||
283 | #endif | ||
284 | xposExtend = iMax(xposExtend, xpos); | ||
285 | xposMax = iMax(xposMax, xposExtend); | ||
286 | if (args->continueFrom_out && ((mode & noWrapFlag_RunMode) || isWrapBoundary_(prevCh, ch))) { | ||
287 | lastWordEnd = currentPos; /* mark word wrap position */ | ||
288 | } | ||
289 | prevCh = ch; | ||
290 | if (--maxLen == 0) { | ||
291 | break; | ||
292 | } | ||
293 | } | ||
294 | if (args->runAdvance_out) { | ||
295 | *args->runAdvance_out = xposMax - orig.x; | ||
296 | } | ||
297 | // fflush(stdout); | ||
298 | return bounds; | ||
299 | } | ||
300 | |||