/***************************************************************************** * Copyright (c) 2014-2025 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "../drawing/Drawing.String.h" #include "../Context.h" #include "../SpriteIds.h" #include "../config/Config.h" #include "../core/CodepointView.hpp" #include "../core/String.hpp" #include "../core/UTF8.h" #include "../core/UnicodeChar.h" #include "../drawing/IDrawingContext.h" #include "../drawing/IDrawingEngine.h" #include "../drawing/Text.h" #include "../interface/ColourWithFlags.h" #include "../interface/Viewport.h" #include "../localisation/Formatting.h" #include "../localisation/LocalisationService.h" #include "../platform/Platform.h" #include "TTF.h" using namespace OpenRCT2; static int32_t TTFGetStringWidth(std::string_view text, FontStyle fontStyle, bool noFormatting); /** * * rct2: 0x006C23B1 */ int32_t GfxGetStringWidthNewLined(std::string_view text, FontStyle fontStyle) { u8string buffer; std::optional maxWidth; FmtString fmt(text); for (const auto& token : fmt) { if (token.kind == FormatToken::newline || token.kind == FormatToken::newlineSmall) { auto width = GfxGetStringWidth(buffer, fontStyle); if (!maxWidth.has_value() || maxWidth.value() > width) { maxWidth = width; } buffer.clear(); } else { buffer.append(token.text); } } if (!maxWidth.has_value()) { maxWidth = GfxGetStringWidth(buffer, fontStyle); } return maxWidth.value(); } /** * Return the width of the string in buffer * * rct2: 0x006C2321 * buffer (esi) */ int32_t GfxGetStringWidth(std::string_view text, FontStyle fontStyle) { return TTFGetStringWidth(text, fontStyle, false); } int32_t GfxGetStringWidthNoFormatting(std::string_view text, FontStyle fontStyle) { return TTFGetStringWidth(text, fontStyle, true); } /** * Clip the text in buffer to width, add ellipsis and return the new width of the clipped string * * rct2: 0x006C2460 * buffer (esi) * width (edi) */ int32_t GfxClipString(utf8* text, int32_t width, FontStyle fontStyle) { if (width < 6) { *text = 0; return 0; } // If width of the full string is less than allowed width then we don't need to clip auto clippedWidth = GfxGetStringWidth(text, fontStyle); if (clippedWidth <= width) { return clippedWidth; } // Append each character 1 by 1 with an ellipsis on the end until width is exceeded thread_local std::string buffer; buffer.clear(); size_t bestLength = 0; int32_t bestWidth = 0; FmtString fmt(text); for (const auto& token : fmt) { CodepointView codepoints(token.text); for (auto codepoint : codepoints) { // Add the ellipsis before checking the width buffer.append("..."); auto currentWidth = GfxGetStringWidth(buffer, fontStyle); if (currentWidth < width) { bestLength = buffer.size(); bestWidth = currentWidth; // Trim the ellipsis buffer.resize(bestLength - 3); } else { // Width exceeded, rollback to best length and put ellipsis back buffer.resize(bestLength); for (auto i = static_cast(bestLength) - 1; i >= 0 && i >= static_cast(bestLength) - 3; i--) { buffer[i] = '.'; } // Copy buffer back to input text buffer std::strcpy(text, buffer.c_str()); return bestWidth; } char cb[8]{}; UTF8WriteCodepoint(cb, codepoint); buffer.append(cb); } } return GfxGetStringWidth(text, fontStyle); } /** * Wrap the text in buffer to width, returns width of longest line. * * Inserts NULL where line should break (as \n is used for something else), * so the number of lines is returned in num_lines. font_height seems to be * a control character for line height. * * rct2: 0x006C21E2 * buffer (esi) * width (edi) - in * num_lines (edi) - out * font_height (ebx) - out */ int32_t GfxWrapString(u8string_view text, int32_t width, FontStyle fontStyle, u8string* outWrappedText, int32_t* outNumLines) { constexpr size_t kNullIndex = std::numeric_limits::max(); u8string buffer; size_t currentLineIndex = 0; size_t splitIndex = kNullIndex; size_t bestSplitIndex = kNullIndex; size_t numLines = 0; int32_t maxWidth = 0; FmtString fmt(text); for (const auto& token : fmt) { if (token.IsLiteral()) { CodepointView codepoints(token.text); for (auto codepoint : codepoints) { char cb[8]{}; UTF8WriteCodepoint(cb, codepoint); buffer.append(cb); auto lineWidth = GfxGetStringWidth(&buffer[currentLineIndex], fontStyle); if (lineWidth <= width || (splitIndex == kNullIndex && bestSplitIndex == kNullIndex)) { if (codepoint == ' ') { // Mark line split here splitIndex = buffer.size() - 1; } else if (splitIndex == kNullIndex) { // Mark line split here (this is after first character of line) bestSplitIndex = buffer.size(); } } else { // Insert new line before current word if (splitIndex == kNullIndex) { splitIndex = bestSplitIndex; } buffer.insert(buffer.begin() + splitIndex, '\0'); // Recalculate the line length after splitting lineWidth = GfxGetStringWidth(&buffer[currentLineIndex], fontStyle); maxWidth = std::max(maxWidth, lineWidth); numLines++; currentLineIndex = splitIndex + 1; splitIndex = kNullIndex; bestSplitIndex = kNullIndex; // Trim the beginning of the new line while (buffer[currentLineIndex] == ' ') { buffer.erase(buffer.begin() + currentLineIndex); } } } } else if (token.kind == FormatToken::newline) { buffer.push_back('\0'); auto lineWidth = GfxGetStringWidth(&buffer[currentLineIndex], fontStyle); maxWidth = std::max(maxWidth, lineWidth); numLines++; currentLineIndex = buffer.size(); splitIndex = kNullIndex; bestSplitIndex = kNullIndex; } else { buffer.append(token.text); } } { // Final line width calculation auto lineWidth = GfxGetStringWidth(&buffer[currentLineIndex], fontStyle); maxWidth = std::max(maxWidth, lineWidth); } if (outWrappedText != nullptr) { *outWrappedText = std::move(buffer); } if (outNumLines != nullptr) { *outNumLines = static_cast(numLines); } return maxWidth; } /** * Draws text that is left aligned and vertically centred. */ void GfxDrawStringLeftCentred( RenderTarget& rt, StringId format, void* args, ColourWithFlags colour, const ScreenCoordsXY& coords) { char buffer[512]; auto bufferPtr = buffer; FormatStringLegacy(bufferPtr, sizeof(buffer), format, args); int32_t height = StringGetHeightRaw(bufferPtr, FontStyle::medium); DrawText(rt, coords - ScreenCoordsXY{ 0, (height / 2) }, { colour }, bufferPtr); } /** * Changes the palette so that the next character changes colour */ static void ColourCharacter(Drawing::TextColour colour, bool withOutline, Drawing::TextColours& textPalette) { auto mapping = Drawing::getTextColourMapping(colour); if (!withOutline) { mapping.sunnyOutline = PaletteIndex::pi0; mapping.shadowOutline = PaletteIndex::pi0; } textPalette = mapping; } /** * Changes the palette so that the next character changes colour * This is specific to changing to a predefined window related colour */ static void ColourCharacterWindow(colour_t colour, bool withOutline, Drawing::TextColours& textPalette) { Drawing::TextColours mapping = { ColourMapA[colour].colour_11, PaletteIndex::pi0, PaletteIndex::pi0, }; if (withOutline) { mapping.sunnyOutline = PaletteIndex::pi10; mapping.shadowOutline = PaletteIndex::pi10; } textPalette = mapping; } /** * * rct2: 0x006C1DB7 * * left : cx * top : dx * numLines : bp * text : esi * dpi : edi */ void DrawStringCentredRaw( RenderTarget& rt, const ScreenCoordsXY& coords, int32_t numLines, const utf8* text, FontStyle fontStyle) { ScreenCoordsXY screenCoords(rt.x, rt.y); DrawText(rt, screenCoords, { COLOUR_BLACK, fontStyle }, ""); screenCoords = coords; for (int32_t i = 0; i <= numLines; i++) { int32_t width = GfxGetStringWidth(text, fontStyle); DrawText(rt, screenCoords - ScreenCoordsXY{ width / 2, 0 }, { kTextColour254, fontStyle }, text); const utf8* ch = text; const utf8* nextCh = nullptr; while ((UTF8GetNext(ch, &nextCh)) != 0) { ch = nextCh; } text = const_cast(ch + 1); screenCoords.y += FontGetLineHeight(fontStyle); } } int32_t StringGetHeightRaw(std::string_view text, FontStyle fontStyle) { int32_t height = 0; if (fontStyle <= FontStyle::medium) height += 10; else if (fontStyle == FontStyle::tiny) height += 6; FmtString fmt(text); for (const auto& token : fmt) { switch (token.kind) { case FormatToken::newline: if (fontStyle == FontStyle::small || fontStyle == FontStyle::medium) { height += 10; break; } if (fontStyle == FontStyle::tiny) { height += 6; break; } height += 18; break; case FormatToken::newlineSmall: if (fontStyle == FontStyle::small || fontStyle == FontStyle::medium) { height += 5; break; } if (fontStyle == FontStyle::tiny) { height += 3; break; } height += 9; break; case FormatToken::fontTiny: fontStyle = FontStyle::tiny; break; case FormatToken::fontMedium: fontStyle = FontStyle::medium; break; case FormatToken::fontSmall: fontStyle = FontStyle::small; break; default: break; } } return height; } /** * * rct2: 0x006C1F57 * * colour : al * format : bx * x : cx * y : dx * text : esi * dpi : edi * width : bp * ticks : ebp >> 16 */ void DrawNewsTicker( RenderTarget& rt, const ScreenCoordsXY& coords, int32_t width, colour_t colour, StringId format, u8string_view args, int32_t ticks) { int32_t numLines, lineHeight, lineY; ScreenCoordsXY screenCoords(rt.x, rt.y); DrawText(rt, screenCoords, { colour }, ""); u8string wrappedString; GfxWrapString(FormatStringID(format, args), width, FontStyle::small, &wrappedString, &numLines); lineHeight = FontGetLineHeight(FontStyle::small); int32_t numCharactersDrawn = 0; int32_t numCharactersToDraw = ticks; const utf8* buffer = wrappedString.data(); lineY = coords.y - ((numLines * lineHeight) / 2); for (int32_t line = 0; line <= numLines; line++) { int32_t halfWidth = GfxGetStringWidth(buffer, FontStyle::small) / 2; FmtString fmt(buffer); for (const auto& token : fmt) { bool doubleBreak = false; if (token.IsLiteral()) { CodepointView codepoints(token.text); for (auto it = codepoints.begin(); it != codepoints.end(); it++) { numCharactersDrawn++; if (numCharactersDrawn > numCharactersToDraw) { auto ch = const_cast(&token.text[it.GetIndex()]); *ch = '\0'; doubleBreak = true; break; } } } if (doubleBreak) break; } screenCoords = { coords.x - halfWidth, lineY }; DrawText(rt, screenCoords, { kTextColour254, FontStyle::small }, buffer); if (numCharactersDrawn > numCharactersToDraw) { break; } buffer = GetStringEnd(buffer) + 1; lineY += lineHeight; } } static void TTFDrawCharacterSprite(RenderTarget& rt, int32_t codepoint, TextDrawInfo* info) { int32_t characterWidth = FontSpriteGetCodepointWidth(info->fontStyle, codepoint); auto sprite = FontSpriteGetCodepointSprite(info->fontStyle, codepoint); if (!info->textDrawFlags.has(TextDrawFlag::noDraw)) { auto screenCoords = ScreenCoordsXY{ info->x, info->y }; if (info->textDrawFlags.has(TextDrawFlag::yOffsetEffect)) { screenCoords.y += *info->yOffset++; } uint8_t palette[8]{}; palette[1] = info->palette.fill; palette[2] = info->palette.sunnyOutline; palette[3] = info->palette.shadowOutline; PaletteMap paletteMap(palette); GfxDrawGlyph(rt, sprite, screenCoords, paletteMap); } info->x += characterWidth; } static void TTFDrawStringRawSprite(RenderTarget& rt, std::string_view text, TextDrawInfo* info) { CodepointView codepoints(text); for (auto codepoint : codepoints) { TTFDrawCharacterSprite(rt, codepoint, info); } } #ifndef DISABLE_TTF static void TTFDrawStringRawTTF(RenderTarget& rt, std::string_view text, TextDrawInfo* info) { if (!TTFInitialise()) return; TTFFontDescriptor* fontDesc = TTFGetFontFromSpriteBase(info->fontStyle); if (fontDesc->font == nullptr) { TTFDrawStringRawSprite(rt, text, info); return; } if (info->textDrawFlags.has(TextDrawFlag::noDraw)) { info->x += TTFGetWidthCacheGetOrAdd(fontDesc->font, text); return; } TTFSurface* surface = TTFSurfaceCacheGetOrAdd(fontDesc->font, text); if (surface == nullptr) return; auto drawingEngine = rt.DrawingEngine; if (drawingEngine != nullptr) { int32_t drawX = info->x + fontDesc->offset_x; int32_t drawY = info->y + fontDesc->offset_y; uint8_t hintThresh = Config::Get().fonts.enableHinting ? fontDesc->hinting_threshold : 0; OpenRCT2::Drawing::IDrawingContext* dc = drawingEngine->GetDrawingContext(); dc->DrawTTFBitmap(rt, info, surface, drawX, drawY, hintThresh); } info->x += surface->w; } #endif // DISABLE_TTF static void TTFProcessFormatCode(RenderTarget& rt, const FmtString::Token& token, TextDrawInfo* info) { switch (token.kind) { case FormatToken::move: info->x = info->startX + token.parameter; break; case FormatToken::newline: info->x = info->startX; info->y += FontGetLineHeight(info->fontStyle); break; case FormatToken::newlineSmall: info->x = info->startX; info->y += FontGetLineHeightSmall(info->fontStyle); break; case FormatToken::fontTiny: info->fontStyle = FontStyle::tiny; break; case FormatToken::fontSmall: info->fontStyle = FontStyle::small; break; case FormatToken::fontMedium: info->fontStyle = FontStyle::medium; break; case FormatToken::outlineEnable: info->colourFlags.set(ColourFlag::withOutline); break; case FormatToken::outlineDisable: info->colourFlags.unset(ColourFlag::withOutline); break; case FormatToken::colourWindow1: { ColourCharacterWindow(gCurrentWindowColours[0], info->colourFlags.has(ColourFlag::withOutline), info->palette); break; } case FormatToken::colourWindow2: { ColourCharacterWindow(gCurrentWindowColours[1], info->colourFlags.has(ColourFlag::withOutline), info->palette); break; } case FormatToken::colourWindow3: { ColourCharacterWindow(gCurrentWindowColours[2], info->colourFlags.has(ColourFlag::withOutline), info->palette); break; } case FormatToken::inlineSprite: { auto imageId = ImageId(token.parameter); auto g1 = GfxGetG1Element(imageId); if (g1 != nullptr && g1->width <= 32 && g1->height <= 32) { if (!info->textDrawFlags.has(TextDrawFlag::noDraw)) { GfxDrawSprite(rt, imageId, { info->x, info->y }); } info->x += g1->width; } break; } default: if (FormatTokenIsColour(token.kind)) { auto colourIndex = FormatTokenToTextColour(token.kind); ColourCharacter(colourIndex, info->colourFlags.has(ColourFlag::withOutline), info->palette); } break; } } #ifndef DISABLE_TTF static bool ShouldUseSpriteForCodepoint(char32_t codepoint) { switch (codepoint) { case UnicodeChar::up: case UnicodeChar::down: case UnicodeChar::leftguillemet: case UnicodeChar::tick: case UnicodeChar::cross: case UnicodeChar::right: case UnicodeChar::rightguillemet: case UnicodeChar::small_up: case UnicodeChar::small_down: case UnicodeChar::left: case UnicodeChar::quote_open: case UnicodeChar::quote_close: case UnicodeChar::german_quote_open: case UnicodeChar::plus: case UnicodeChar::minus: case UnicodeChar::variation_selector: case UnicodeChar::eye: case UnicodeChar::road: case UnicodeChar::railway: return true; default: return false; } } #endif // DISABLE_TTF static void TTFProcessStringLiteral(RenderTarget& rt, std::string_view text, TextDrawInfo* info) { #ifndef DISABLE_TTF bool isTTF = info->textDrawFlags.has(TextDrawFlag::ttf); #else bool isTTF = false; #endif // DISABLE_TTF if (!isTTF) { TTFDrawStringRawSprite(rt, text, info); } #ifndef DISABLE_TTF else { CodepointView codepoints(text); std::optional ttfRunIndex{}; for (auto it = codepoints.begin(); it != codepoints.end(); it++) { auto codepoint = *it; if (ShouldUseSpriteForCodepoint(codepoint)) { if (ttfRunIndex.has_value()) { // Draw the TTF run // This error suppression abomination is here to suppress https://github.com/OpenRCT2/OpenRCT2/issues/17371. // Additionally, we have to suppress the error for the error suppression... :'-( // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105937 is fixed in GCC13 #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #endif auto len = it.GetIndex() - ttfRunIndex.value(); TTFDrawStringRawTTF(rt, text.substr(ttfRunIndex.value(), len), info); #if defined(__GNUC__) && !defined(__clang__) #pragma GCC diagnostic pop #endif ttfRunIndex = std::nullopt; } // Draw the sprite font glyph TTFDrawCharacterSprite(rt, codepoint, info); } else { if (!ttfRunIndex.has_value()) { ttfRunIndex = it.GetIndex(); } } } if (ttfRunIndex.has_value()) { // Final TTF run auto len = text.size() - *ttfRunIndex; TTFDrawStringRawTTF(rt, text.substr(ttfRunIndex.value(), len), info); } } #endif // DISABLE_TTF } static void TTFProcessStringCodepoint(RenderTarget& rt, codepoint_t codepoint, TextDrawInfo* info) { char buffer[8]{}; UTF8WriteCodepoint(buffer, codepoint); TTFProcessStringLiteral(rt, buffer, info); } static void TTFProcessString(RenderTarget& rt, std::string_view text, TextDrawInfo* info) { if (info->textDrawFlags.has(TextDrawFlag::noFormatting)) { TTFProcessStringLiteral(rt, text, info); info->maxX = std::max(info->maxX, info->x); info->maxY = std::max(info->maxY, info->y); } else { FmtString fmt(text); for (const auto& token : fmt) { if (token.IsLiteral()) { TTFProcessStringLiteral(rt, token.text, info); } else if (token.IsCodepoint()) { auto codepoint = token.GetCodepoint(); TTFProcessStringCodepoint(rt, codepoint, info); } else { TTFProcessFormatCode(rt, token, info); } info->maxX = std::max(info->maxX, info->x); info->maxY = std::max(info->maxY, info->y); } } } static void TTFProcessInitialColour(ColourWithFlags colour, TextDrawInfo* info) { if (colour.colour != kTextColour254 && colour.colour != kTextColour255) { info->colourFlags = colour.flags; if (!colour.flags.has(ColourFlag::inset)) { ColourCharacterWindow(colour.colour, info->colourFlags.has(ColourFlag::withOutline), info->palette); } else { Drawing::TextColours newPalette = {}; switch (info->darkness) { case TextDarkness::extraDark: newPalette.fill = ColourMapA[colour.colour].dark; newPalette.shadowOutline = ColourMapA[colour.colour].mid_light; break; case TextDarkness::dark: newPalette.fill = ColourMapA[colour.colour].mid_dark; newPalette.shadowOutline = ColourMapA[colour.colour].light; break; case TextDarkness::regular: newPalette.fill = ColourMapA[colour.colour].mid_light; newPalette.shadowOutline = ColourMapA[colour.colour].lighter; break; } info->palette = newPalette; } } } void TTFDrawString( RenderTarget& rt, const_utf8string text, ColourWithFlags colour, const ScreenCoordsXY& coords, bool noFormatting, FontStyle fontStyle, TextDarkness darkness) { if (text == nullptr) return; TextDrawInfo info{}; info.fontStyle = fontStyle; info.startX = coords.x; info.startY = coords.y; info.x = coords.x; info.y = coords.y; info.darkness = darkness; if (LocalisationService_UseTrueTypeFont()) { info.textDrawFlags.set(TextDrawFlag::ttf); } if (noFormatting) { info.textDrawFlags.set(TextDrawFlag::noFormatting); } info.palette = gTextPalette; TTFProcessInitialColour(colour, &info); TTFProcessString(rt, text, &info); gTextPalette = info.palette; rt.lastStringPos = { info.x, info.y }; } static int32_t TTFGetStringWidth(std::string_view text, FontStyle fontStyle, bool noFormatting) { TextDrawInfo info{}; info.fontStyle = fontStyle; info.startX = 0; info.startY = 0; info.x = 0; info.y = 0; info.maxX = 0; info.maxY = 0; info.textDrawFlags.set(TextDrawFlag::noDraw); if (LocalisationService_UseTrueTypeFont()) { info.textDrawFlags.set(TextDrawFlag::ttf); } if (noFormatting) { info.textDrawFlags.set(TextDrawFlag::noFormatting); } RenderTarget dummy{}; TTFProcessString(dummy, text, &info); return info.maxX; } /** * * rct2: 0x00682F28 */ void GfxDrawStringWithYOffsets( RenderTarget& rt, const utf8* text, ColourWithFlags colour, const ScreenCoordsXY& coords, const int8_t* yOffsets, bool forceSpriteFont, FontStyle fontStyle) { TextDrawInfo info{}; info.fontStyle = fontStyle; info.startX = coords.x; info.startY = coords.y; info.x = coords.x; info.y = coords.y; info.yOffset = yOffsets; info.textDrawFlags.set(TextDrawFlag::yOffsetEffect); if (!forceSpriteFont && LocalisationService_UseTrueTypeFont()) { info.textDrawFlags.set(TextDrawFlag::ttf); } info.palette = gTextPalette; TTFProcessInitialColour(colour, &info); TTFProcessString(rt, text, &info); gTextPalette = info.palette; rt.lastStringPos = { info.x, info.y }; } u8string ShortenPath(const u8string& path, int32_t availableWidth, FontStyle fontStyle) { if (GfxGetStringWidth(path, fontStyle) <= availableWidth) { return path; } u8string shortenedPath = u8"..."; size_t begin = 0; while (begin < path.size()) { begin = path.find_first_of(*PATH_SEPARATOR, begin + 1); if (begin == path.npos) break; shortenedPath = u8"..." + path.substr(begin); if (GfxGetStringWidth(shortenedPath, fontStyle) <= availableWidth) { return shortenedPath; } } return shortenedPath; }