/* Copyright 2020 Jaakko Keränen Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* this file is included from text.c, so it doesn't use includes of its own */ iLocalDef iBool isWrapPunct_(iChar c) { /* Punctuation that participates in word-wrapping. */ return (c == '/' || c == '\\' || c == '=' || c == '-' || c == ',' || c == ';' || c == '.' || c == ':' || c == 0xad); } iLocalDef iBool isClosingBracket_(iChar c) { return (c == ')' || c == ']' || c == '}' || c == '>'); } iLocalDef iBool isWrapBoundary_(iChar prevC, iChar c) { /* Line wrapping boundaries are determined by looking at a character and the last character processed. We want to wrap at natural word boundaries where possible, so normally we wrap at a space followed a non-space character. As an exception, we also wrap after punctuation used to break up words, so we can wrap text like foo/bar/baz-abc-def.xyz at any puncation boundaries, without wrapping on other punctuation used for expressive purposes like emoticons :-) */ if (isClosingBracket_(prevC) && !isWrapPunct_(c)) { return iTrue; } if (isSpace_Char(prevC)) { return iFalse; } if ((prevC == '/' || prevC == '\\' || prevC == '-' || prevC == '_' || prevC == '+') && !isWrapPunct_(c)) { return iTrue; } return isSpace_Char(c); } iLocalDef iBool isMeasuring_(enum iRunMode mode) { return (mode & modeMask_RunMode) == measure_RunMode; } static iRect runSimple_Font_(iFont *d, const iRunArgs *args) { /* This function shapes text using a simplified, incomplete algorithm. It works for English and other non-complex LTR scripts. Composed glyphs are not supported (must rely on text being in a pre-composed form). This algorithm is used if HarfBuzz is not available. */ const iInt2 orig = args->pos; iRect bounds = { orig, init_I2(0, d->height) }; float xpos = orig.x; float xposMax = xpos; float monoAdvance = 0; int ypos = orig.y; size_t maxLen = args->maxLen ? args->maxLen : iInvalidSize; float xposExtend = orig.x; /* allows wide glyphs to use more space; restored by whitespace */ iWrapText * wrap = args->wrap; int wrapAdvance = 0; const int xposLimit = (wrap && wrap->maxWidth ? orig.x + wrap->maxWidth : 0); const enum iRunMode mode = args->mode; const char * lastWordEnd = args->text.start; //iAssert(xposLimit == 0 || isMeasuring_(mode)); iAssert(args->text.end >= args->text.start); if (wrap) { wrap->wrapRange_ = args->text; wrap->hitAdvance_out = zero_I2(); wrap->hitChar_out = NULL; wrap->hitGlyphNormX_out = 0.0f; } // if (args->continueFrom_out) { // *args->continueFrom_out = args->text.end; // } const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); const iBool checkHitChar = wrap && wrap->hitChar; const iBool isMonospaced = d->isMonospaced && !(mode & alwaysVariableWidthFlag_RunMode); if (isMonospaced) { monoAdvance = glyph_Font_(d, 'M')->advance; } if (args->mode & fillBackground_RunMode) { const iColor initial = get_Color(args->color); SDL_SetRenderDrawColor(text_.render, initial.r, initial.g, initial.b, 0); } /* Text rendering is not very straightforward! Let's dive in... */ iChar prevCh = 0; const char *chPos; for (chPos = args->text.start; chPos != args->text.end; ) { iAssert(chPos < args->text.end); const char *currentPos = chPos; const iBool isHitPointOnThisLine = (checkHitPoint && wrap->hitPoint.y >= ypos && wrap->hitPoint.y < ypos + d->height); if (checkHitChar && currentPos == wrap->hitChar) { wrap->hitAdvance_out = sub_I2(init_I2(xpos, ypos), orig); } /* Check if the hit point is on the left side of the line. */ if (isHitPointOnThisLine && !wrap->hitChar_out && wrap->hitPoint.x < orig.x) { wrap->hitChar_out = currentPos; wrap->hitGlyphNormX_out = 0.0f; } if (*chPos == 0x1b) { /* ANSI escape. */ chPos++; iRegExpMatch m; init_RegExpMatch(&m); if (match_RegExp(text_.ansiEscape, chPos, args->text.end - chPos, &m)) { if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { /* Change the color. */ const iColor clr = ansiForeground_Color(capturedRange_RegExpMatch(&m, 1), tmParagraph_ColorId); SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); if (args->mode & fillBackground_RunMode) { SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); } } chPos = end_RegExpMatch(&m); continue; } } iChar ch = nextChar_(&chPos, args->text.end); iBool isEmoji = isEmoji_Char(ch); if (ch == 0x200d) { /* zero-width joiner */ /* We don't have the composited Emojis. */ if (isEmoji_Char(prevCh)) { /* skip */ nextChar_(&chPos, args->text.end); ch = nextChar_(&chPos, args->text.end); } } if (isVariationSelector_Char(ch)) { ch = nextChar_(&chPos, args->text.end); /* skip it */ } /* Special instructions. */ { if (ch == 0xad) { /* soft hyphen */ lastWordEnd = chPos; if (isMeasuring_(mode)) { if (xposLimit > 0) { const char *postHyphen = chPos; iChar nextCh = nextChar_(&postHyphen, args->text.end); if ((int) xpos + glyph_Font_(d, ch)->rect[0].size.x + glyph_Font_(d, nextCh)->rect[0].size.x > xposLimit) { /* Wraps after hyphen, should show it. */ } else continue; } else continue; } else { /* Only show it at the end. */ if (chPos != args->text.end) { continue; } } } /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */ if (ch == '\n') { /* Notify about the wrap. */ if (!notify_WrapText_(wrap, chPos, 0, iMax(xpos, xposExtend) - orig.x, iFalse)) { break; } lastWordEnd = NULL; xpos = xposExtend = orig.x; ypos += d->height; prevCh = ch; continue; } if (ch == '\t') { const int tabStopWidth = d->height * 10; const int halfWidth = (xposLimit - orig.x) / 2; const int xRel = xpos - orig.x; /* First stop is always to half width. */ if (halfWidth > 0 && xRel < halfWidth) { xpos = orig.x + halfWidth; } else if (halfWidth > 0 && xRel < halfWidth * 3 / 2) { xpos = orig.x + halfWidth * 3 / 2; } else { xpos = orig.x + ((xRel / tabStopWidth) + 1) * tabStopWidth; } xposExtend = iMax(xposExtend, xpos); prevCh = 0; continue; } if (ch == '\v') { /* color change */ iChar esc = nextChar_(&chPos, args->text.end); int colorNum = args->color; if (esc == '\v') { /* Extended range. */ esc = nextChar_(&chPos, args->text.end) + asciiExtended_ColorEscape; colorNum = esc - asciiBase_ColorEscape; } else if (esc != 0x24) { /* ASCII Cancel */ colorNum = esc - asciiBase_ColorEscape; } if (mode & draw_RunMode && ~mode & permanentColorFlag_RunMode) { const iColor clr = get_Color(colorNum); SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); if (args->mode & fillBackground_RunMode) { SDL_SetRenderDrawColor(text_.render, clr.r, clr.g, clr.b, 0); } } prevCh = 0; continue; } if (isDefaultIgnorable_Char(ch) || isFitzpatrickType_Char(ch)) { continue; } } const iGlyph *glyph = glyph_Font_(d, ch); int x1 = iMax(xpos, xposExtend); /* Which half of the pixel the glyph falls on? */ const int hoff = enableHalfPixelGlyphs_Text ? (xpos - x1 > 0.5f ? 1 : 0) : 0; if (mode & draw_RunMode && ch != 0x20 && ch != 0 && !isRasterized_Glyph_(glyph, hoff)) { /* Need to pause here and make sure all glyphs have been cached in the text. */ // printf("[Text] missing from cache: %lc (%x)\n", (int) ch, ch); //cacheTextGlyphs_Font_(d, args->text); cacheSingleGlyph_Font_(glyph->font, index_Glyph_(glyph)); glyph = glyph_Font_(d, ch); /* cache may have been reset */ } int x2 = x1 + glyph->rect[hoff].size.x; if (isHitPointOnThisLine) { if (wrap->hitPoint.x >= x1) { /* may also be off to the right */ wrap->hitChar_out = currentPos; wrap->hitGlyphNormX_out = wrap->hitPoint.x < x2 ? (wrap->hitPoint.x - x1) / glyph->advance : 1.0f; } } /* Out of the allotted space on the line? */ if (xposLimit > 0 && x2 > xposLimit) { iAssert(wrap); const char *wrapPos = currentPos; int advance = x1 - orig.x; if (lastWordEnd && wrap->mode == word_WrapTextMode) { wrapPos = skipSpace_CStr(lastWordEnd); /* go back */ wrapPos = iMin(wrapPos, args->text.end); advance = wrapAdvance; } if (!notify_WrapText_(wrap, wrapPos, 0, advance, iFalse)) { break; } lastWordEnd = NULL; xpos = xposExtend = orig.x; ypos += d->height; prevCh = 0; chPos = wrapPos; continue; } const int yLineMax = ypos + d->height; SDL_Rect dst = { x1 + glyph->d[hoff].x, ypos + glyph->font->baseline + glyph->d[hoff].y, glyph->rect[hoff].size.x, glyph->rect[hoff].size.y }; if (glyph->font != d) { if (glyph->font->height > d->height) { /* Center-align vertically so the baseline isn't totally offset. */ dst.y -= (glyph->font->height - d->height) / 2; } } /* Update the bounding box. */ if (mode & visualFlag_RunMode) { if (isEmpty_Rect(bounds)) { bounds = init_Rect(dst.x, dst.y, dst.w, dst.h); } else { bounds = union_Rect(bounds, init_Rect(dst.x, dst.y, dst.w, dst.h)); } } else { bounds.size.x = iMax(bounds.size.x, x2 - orig.x); bounds.size.y = iMax(bounds.size.y, ypos + glyph->font->height - orig.y); } /* Symbols and emojis are NOT monospaced, so must conform when the primary font is monospaced. Except with Japanese script, that's larger than the normal monospace. */ const iBool useMonoAdvance = monoAdvance > 0 && !isJapanese_FontId(fontId_Text_(glyph->font)); const float advance = (useMonoAdvance && glyph->advance > 0 ? monoAdvance : glyph->advance); if (!isMeasuring_(mode) && ch != 0x20 /* don't bother rendering spaces */) { if (useMonoAdvance && dst.w > advance && glyph->font != d && !isEmoji) { /* Glyphs from a different font may need recentering to look better. */ dst.x -= (dst.w - advance) / 2; } SDL_Rect src; memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect)); /* Clip the glyphs to the font's height. This is useful when the font's line spacing has been reduced or when the glyph is from a different font. */ if (dst.y + dst.h > yLineMax) { const int over = dst.y + dst.h - yLineMax; src.h -= over; dst.h -= over; } if (dst.y < ypos) { const int over = ypos - dst.y; dst.y += over; dst.h -= over; src.y += over; src.h -= over; } dst.x += origin_Paint.x; dst.y += origin_Paint.y; if (args->mode & fillBackground_RunMode) { /* Alpha blending looks much better if the RGB components don't change in the partially transparent pixels. */ SDL_RenderFillRect(text_.render, &dst); } SDL_RenderCopy(text_.render, text_.cache, &src, &dst); } xpos += advance; if (!isSpace_Char(ch)) { xposExtend += isEmoji ? glyph->advance : advance; } #if defined (LAGRANGE_ENABLE_KERNING) /* Check the next character. */ if (!isMonospaced && glyph->font == d) { /* TODO: No need to decode the next char twice; check this on the next iteration. */ const char *peek = chPos; const iChar next = nextChar_(&peek, args->text.end); if (enableKerning_Text && next) { const uint32_t nextGlyphIndex = glyphIndex_Font_(glyph->font, next); int kern = stbtt_GetGlyphKernAdvance( &glyph->font->font, index_Glyph_(glyph), nextGlyphIndex); /* Nunito needs some kerning fixes. */ if (glyph->font->family == nunito_TextFont) { if (ch == 'W' && (next == 'i' || next == 'h')) { kern = -30; } else if (ch == 'T' && next == 'h') { kern = -15; } else if (ch == 'V' && next == 'i') { kern = -15; } } if (kern) { // printf("%lc(%u) -> %lc(%u): kern %d (%f)\n", ch, glyph->glyphIndex, next, // nextGlyphIndex, // kern, d->xScale * kern); xpos += glyph->font->xScale * kern; xposExtend += glyph->font->xScale * kern; } } } #endif xposExtend = iMax(xposExtend, xpos); xposMax = iMax(xposMax, xposExtend); if ((wrap && wrap->mode == anyCharacter_WrapTextMode) || isWrapBoundary_(prevCh, ch)) { lastWordEnd = currentPos; /* mark word wrap position */ wrapAdvance = x2 - orig.x; } prevCh = ch; if (--maxLen == 0) { break; } } notify_WrapText_(wrap, chPos, 0, xpos - orig.x, iFalse); if (checkHitChar && wrap->hitChar == args->text.end) { wrap->hitAdvance_out = sub_I2(init_I2(xpos, ypos), orig); } if (args->cursorAdvance_out) { *args->cursorAdvance_out = sub_I2(init_I2(xpos, ypos), orig); } if (args->runAdvance_out) { *args->runAdvance_out = xposMax - orig.x; } // fflush(stdout); return bounds; }