summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-06-26 07:02:46 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-06-26 07:02:46 +0300
commit512702de740c3c6b38097d101f1f454d7da22e97 (patch)
treef0562a6872131fcc271d4838d3761a67408d62e7
parent5dbc85eaaa1bd0a0fc11dd76a75ece2efe763df5 (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.txt5
-rw-r--r--Depends.cmake12
-rw-r--r--src/ui/text.c277
-rw-r--r--src/ui/text_simple.c300
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)
29endif () 29endif ()
30 30
31# Build configuration. 31# Build configuration.
32option (ENABLE_HARFBUZZ "Use HarfBuzz to shape text" ON)
32option (ENABLE_IPC "Use IPC to communicate between running instances" ON) 33option (ENABLE_IPC "Use IPC to communicate between running instances" ON)
33option (ENABLE_MPG123 "Use mpg123 for decoding MPEG audio" ON) 34option (ENABLE_MPG123 "Use mpg123 for decoding MPEG audio" ON)
34option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF) 35option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF)
@@ -308,6 +309,10 @@ if (ENABLE_CUSTOM_FRAME AND MSYS)
308endif () 309endif ()
309target_link_libraries (app PUBLIC the_Foundation::the_Foundation) 310target_link_libraries (app PUBLIC the_Foundation::the_Foundation)
310target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) 311target_link_libraries (app PUBLIC ${SDL2_LDFLAGS})
312if (ENABLE_HARFBUZZ AND HARFBUZZ_FOUND)
313 target_link_libraries (app PUBLIC harfbuzz)
314 target_compile_definitions (app PUBLIC LAGRANGE_ENABLE_HARFBUZZ=1)
315endif ()
311if (APPLE) 316if (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 ()
4endif () 4endif ()
5 5
6if (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)
16endif ()
17
6if (NOT EXISTS ${CMAKE_SOURCE_DIR}/lib/the_Foundation/CMakeLists.txt) 18if (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)
912static iRect run_Font_(iFont *d, const iRunArgs *args) { 917static 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
1185int lineHeight_Text(int fontId) { 928int 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
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. 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
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY 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
21SOFTWARE, 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
25static 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