summaryrefslogtreecommitdiff
path: root/src/ui/text_simple.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/text_simple.c')
-rw-r--r--src/ui/text_simple.c300
1 files changed, 300 insertions, 0 deletions
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