/* * This file is part of OpenTTD. * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . */ /** @file gfx_layout.cpp Handling of laying out text. */ #include "stdafx.h" #include "core/math_func.hpp" #include "gfx_layout.h" #include "string_func.h" #include "debug.h" #include "table/control_codes.h" #include "gfx_layout_fallback.h" #if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ) #include "gfx_layout_icu.h" #endif /* WITH_ICU_I18N && WITH_HARFBUZZ */ #ifdef WITH_UNISCRIBE #include "os/windows/string_uniscribe.h" #endif /* WITH_UNISCRIBE */ #ifdef WITH_COCOA #include "os/macosx/string_osx.h" #endif #include "safeguards.h" /** Cache of ParagraphLayout lines. */ Layouter::LineCache *Layouter::linecache; /** Cache of Font instances. */ Layouter::FontColourMap Layouter::fonts[FS_END]; /** * Construct a new font. * @param size The font size to use for this font. * @param colour The colour to draw this font in. */ Font::Font(FontSize size, TextColour colour) : fc(FontCache::Get(size)), colour(colour) { assert(size < FS_END); } /** * Helper for getting a ParagraphLayouter of the given type. * * @note In case no ParagraphLayouter could be constructed, line.layout will be nullptr. * @param line The cache item to store our layouter in. * @param str The string to create a layouter for. * @param state The state of the font and color. * @tparam T The type of layouter we want. */ template static inline void GetLayouter(Layouter::LineCacheItem &line, std::string_view str, FontState &state) { if (line.buffer != nullptr) free(line.buffer); typename T::CharType *buff_begin = MallocT(str.size() + 1); const typename T::CharType *buffer_last = buff_begin + str.size() + 1; typename T::CharType *buff = buff_begin; FontMap &fontMapping = line.runs; Font *f = Layouter::GetFont(state.fontsize, state.cur_colour); line.buffer = buff_begin; fontMapping.clear(); auto cur = str.begin(); /* * Go through the whole string while adding Font instances to the font map * whenever the font changes, and convert the wide characters into a format * usable by ParagraphLayout. */ for (; buff < buffer_last && cur != str.end();) { char32_t c = Utf8Consume(cur); if (c == '\0' || c == '\n') { /* Caller should already have filtered out these characters. */ NOT_REACHED(); } else if (c >= SCC_BLUE && c <= SCC_BLACK) { state.SetColour((TextColour)(c - SCC_BLUE)); } else if (c == SCC_PUSH_COLOUR) { state.PushColour(); } else if (c == SCC_POP_COLOUR) { state.PopColour(); } else if (c >= SCC_FIRST_FONT && c <= SCC_LAST_FONT) { state.SetFontSize((FontSize)(c - SCC_FIRST_FONT)); } else { /* Filter out non printable characters */ if (!IsPrintable(c)) continue; /* Filter out text direction characters that shouldn't be drawn, and * will not be handled in the fallback case because they are mostly * needed for RTL languages which need more proper shaping support. */ if (!T::SUPPORTS_RTL && IsTextDirectionChar(c)) continue; buff += T::AppendToBuffer(buff, buffer_last, c); continue; } if (fontMapping.count(buff - buff_begin) == 0) { fontMapping[buff - buff_begin] = f; } f = Layouter::GetFont(state.fontsize, state.cur_colour); } /* Better safe than sorry. */ *buff = '\0'; if (fontMapping.count(buff - buff_begin) == 0) { fontMapping[buff - buff_begin] = f; } line.layout = T::GetParagraphLayout(buff_begin, buff, fontMapping); line.state_after = state; } /** * Create a new layouter. * @param str The string to create the layout for. * @param maxw The maximum width. * @param colour The colour of the font. * @param fontsize The size of font to use. */ Layouter::Layouter(std::string_view str, int maxw, TextColour colour, FontSize fontsize) : string(str) { FontState state(colour, fontsize); while (true) { auto line_length = str.find_first_of('\n'); auto str_line = str.substr(0, line_length); LineCacheItem &line = GetCachedParagraphLayout(str_line, state); if (line.layout != nullptr) { state = line.state_after; line.layout->Reflow(); } else { /* Line is new, layout it */ FontState old_state = state; #if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ) if (line.layout == nullptr) { GetLayouter(line, str_line, state); if (line.layout == nullptr) { state = old_state; } } #endif #ifdef WITH_UNISCRIBE if (line.layout == nullptr) { GetLayouter(line, str_line, state); if (line.layout == nullptr) { state = old_state; } } #endif #ifdef WITH_COCOA if (line.layout == nullptr) { GetLayouter(line, str_line, state); if (line.layout == nullptr) { state = old_state; } } #endif if (line.layout == nullptr) { GetLayouter(line, str_line, state); } } /* Move all lines into a local cache so we can reuse them later on more easily. */ for (;;) { auto l = line.layout->NextLine(maxw); if (l == nullptr) break; this->push_back(std::move(l)); } /* Break out if this was the last line. */ if (line_length == std::string_view::npos) { break; } /* Go to the next line. */ str.remove_prefix(line_length + 1); } } /** * Get the boundaries of this paragraph. * @return The boundaries. */ Dimension Layouter::GetBounds() { Dimension d = { 0, 0 }; for (const auto &l : *this) { d.width = std::max(d.width, l->GetWidth()); d.height += l->GetLeading(); } return d; } /** * Test whether a character is a non-printable formatting code */ static bool IsConsumedFormattingCode(char32_t ch) { if (ch >= SCC_BLUE && ch <= SCC_BLACK) return true; if (ch == SCC_PUSH_COLOUR) return true; if (ch == SCC_POP_COLOUR) return true; if (ch >= SCC_FIRST_FONT && ch <= SCC_LAST_FONT) return true; // All other characters defined in Unicode standard are assumed to be non-consumed. return false; } /** * Get the position of a character in the layout. * @param ch Character to get the position of. Must be an iterator of the string passed to the constructor. * @return Upper left corner of the character relative to the start of the string. * @note Will only work right for single-line strings. */ Point Layouter::GetCharPosition(std::string_view::const_iterator ch) const { const auto &line = this->front(); /* Pointer to the end-of-string marker? Return total line width. */ if (ch == this->string.end()) { Point p = { line->GetWidth(), 0 }; return p; } /* Find the code point index which corresponds to the char * pointer into our UTF-8 source string. */ size_t index = 0; auto str = this->string.begin(); while (str < ch) { char32_t c = Utf8Consume(str); if (!IsConsumedFormattingCode(c)) index += line->GetInternalCharLength(c); } /* Initial position, returned if character not found. */ static const std::vector zero = { {0, 0} }; auto position = zero.begin(); /* We couldn't find the code point index. */ if (str != ch) return *position; /* Valid character. */ /* Scan all runs until we've found our code point index. */ for (int run_index = 0; run_index < line->CountRuns(); run_index++) { const ParagraphLayouter::VisualRun &run = line->GetVisualRun(run_index); const auto &positions = run.GetPositions(); const auto &charmap = run.GetGlyphToCharMap(); /* Run starts after our character, use the last found position. */ if (static_cast(charmap.front()) > index) return *position; position = positions.begin(); for (auto it = charmap.begin(); it != charmap.end(); /* nothing */) { /* Plain honest-to-$deity match. */ if (static_cast(*it) == index) return *position; ++it; if (it == charmap.end()) break; /* We just passed our character, it's probably a ligature, use the last found position. */ if (static_cast(*it) > index) return *position; ++position; } } /* At the end of the run but still didn't find our character so probably a trailing ligature, use the last found position. */ return *position; } /** * Get the character that is at a pixel position in the first line of the layouted text. * @param x Position in the string. * @param line_index Which line of the layout to search * @return String offset of the position (bytes) or -1 if no character is at the position. */ ptrdiff_t Layouter::GetCharAtPosition(int x, size_t line_index) const { if (line_index >= this->size()) return -1; const auto &line = this->at(line_index); for (int run_index = 0; run_index < line->CountRuns(); run_index++) { const ParagraphLayouter::VisualRun &run = line->GetVisualRun(run_index); const auto &glyphs = run.GetGlyphs(); const auto &positions = run.GetPositions(); const auto &charmap = run.GetGlyphToCharMap(); for (int i = 0; i < run.GetGlyphCount(); i++) { /* Not a valid glyph (empty). */ if (glyphs[i] == 0xFFFF) continue; int begin_x = positions[i].x; int end_x = positions[i + 1].x; if (IsInsideMM(x, begin_x, end_x)) { /* Found our glyph, now convert to UTF-8 string index. */ size_t index = charmap[i]; size_t cur_idx = 0; for (auto str = this->string.begin(); str != this->string.end();) { if (cur_idx == index) return str - this->string.begin(); char32_t c = Utf8Consume(str); if (!IsConsumedFormattingCode(c)) cur_idx += line->GetInternalCharLength(c); } } } } return -1; } /** * Get a static font instance. */ Font *Layouter::GetFont(FontSize size, TextColour colour) { FontColourMap::iterator it = fonts[size].find(colour); if (it != fonts[size].end()) return it->second.get(); fonts[size][colour] = std::make_unique(size, colour); return fonts[size][colour].get(); } /** * Perform initialization of layout engine. */ void Layouter::Initialize() { #if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ) ICUParagraphLayoutFactory::InitializeLayouter(); #endif /* WITH_ICU_I18N && WITH_HARFBUZZ */ } /** * Reset cached font information. * @param size Font size to reset. */ void Layouter::ResetFontCache(FontSize size) { fonts[size].clear(); /* We must reset the linecache since it references the just freed fonts */ ResetLineCache(); #if defined(WITH_UNISCRIBE) UniscribeResetScriptCache(size); #endif #if defined(WITH_COCOA) MacOSResetScriptCache(size); #endif } /** * Get reference to cache item. * If the item does not exist yet, it is default constructed. * @param str Source string of the line (including colour and font size codes). * @param state State of the font at the beginning of the line. * @return Reference to cache item. */ Layouter::LineCacheItem &Layouter::GetCachedParagraphLayout(std::string_view str, const FontState &state) { if (linecache == nullptr) { /* Create linecache on first access to avoid trouble with initialisation order of static variables. */ linecache = new LineCache(); } if (auto match = linecache->find(LineCacheQuery{state, str}); match != linecache->end()) { return match->second; } /* Create missing entry */ LineCacheKey key; key.state_before = state; key.str.assign(str); return (*linecache)[key]; } /** * Clear line cache. */ void Layouter::ResetLineCache() { if (linecache != nullptr) linecache->clear(); } /** * Reduce the size of linecache if necessary to prevent infinite growth. */ void Layouter::ReduceLineCache() { if (linecache != nullptr) { /* TODO LRU cache would be fancy, but not exactly necessary */ if (linecache->size() > 4096) ResetLineCache(); } }