OpenTTD Source 20251213-master-g1091fa6071
gfx_layout.cpp
Go to the documentation of this file.
1/*
2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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 <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
10#include "stdafx.h"
11#include "core/math_func.hpp"
12#include "gfx_layout.h"
13#include "gfx_func.h"
14#include "string_func.h"
15#include "strings_func.h"
16#include "core/utf8.hpp"
17#include "debug.h"
18#include "timer/timer.h"
19#include "timer/timer_window.h"
20#include "viewport_func.h"
21#include "window_func.h"
22
23#include "table/control_codes.h"
24
25#include "gfx_layout_fallback.h"
26
27#if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ)
28#include "gfx_layout_icu.h"
29#endif /* WITH_ICU_I18N && WITH_HARFBUZZ */
30
31#ifdef WITH_UNISCRIBE
33#endif /* WITH_UNISCRIBE */
34
35#ifdef WITH_COCOA
37#endif
38
39#include "safeguards.h"
40
41
43std::unique_ptr<Layouter::LineCache> Layouter::linecache;
44
46 std::array<std::set<char32_t>, FS_END> glyphs{};
47public:
49
50 FontLoadReason GetLoadReason() override { return FontLoadReason::MissingFallback; }
51
52 inline void Insert(FontSize fs, char32_t c)
53 {
54 this->glyphs[fs].insert(c);
55 this->search_timeout.Reset();
56 }
57
58 std::set<char32_t> GetRequiredGlyphs(FontSizes fontsizes) override
59 {
60 std::set<char32_t> r;
61 for (FontSize fs : fontsizes) {
62 r.merge(this->glyphs[fs]);
63 }
64 return r;
65 }
66
67 TimeoutTimer<TimerWindow> search_timeout{std::chrono::milliseconds(250), [this]()
68 {
69 FontSizes changed_fontsizes{};
70 for (FontSize fs = FS_BEGIN; fs != FS_END; ++fs) {
71 auto &missing = this->glyphs[fs];
72 if (missing.empty()) continue;
73
74 if (FontProviderManager::FindFallbackFont({}, fs, this)) changed_fontsizes.Set(fs);
75 missing.clear();
76 }
77
78 if (!changed_fontsizes.Any()) return;
79
80 FontCache::LoadFontCaches(changed_fontsizes);
81 LoadStringWidthTable(changed_fontsizes);
83 ReInitAllWindows(true);
84 }};
85};
86
87static RuntimeMissingGlyphSearcher _missing_glyphs;
88
98template <typename T>
99static inline void GetLayouter(Layouter::LineCacheItem &line, std::string_view str, FontState &state)
100{
101 typename T::CharType *buff_begin = new typename T::CharType[str.size() + 1];
102 /* Move ownership of buff_begin into the Buffer/unique_ptr. */
103 line.buffer = Layouter::LineCacheItem::Buffer(buff_begin, [](void *p) { delete[] reinterpret_cast<T::CharType *>(p); });
104
105 const typename T::CharType *buffer_last = buff_begin + str.size() + 1;
106 typename T::CharType *buff = buff_begin;
107 FontMap &font_mapping = line.runs;
108 Font f{state.font_index, state.cur_colour};
109
110 font_mapping.clear();
111
112 /*
113 * Go through the whole string while adding Font instances to the font map
114 * whenever the font changes, and convert the wide characters into a format
115 * usable by ParagraphLayout.
116 */
117 Utf8View view(str);
118 for (auto it = view.begin(); it != view.end(); /* nothing */) {
119 auto cur = it;
120 uint32_t c = *it++;
121 if (c == '\0' || c == '\n') {
122 /* Caller should already have filtered out these characters. */
123 NOT_REACHED();
124 } else if (c >= SCC_BLUE && c <= SCC_BLACK) {
125 state.SetColour((TextColour)(c - SCC_BLUE));
126 } else if (c == SCC_PUSH_COLOUR) {
127 state.PushColour();
128 } else if (c == SCC_POP_COLOUR) {
129 state.PopColour();
130 } else if (c >= SCC_FIRST_FONT && c <= SCC_LAST_FONT) {
131 state.SetFontSize((FontSize)(c - SCC_FIRST_FONT));
132 } else {
133 /* Filter out non printable characters */
134 if (!IsPrintable(c)) continue;
135
136 if (IsTextDirectionChar(c)) {
137 /* Filter out text direction characters that shouldn't be drawn, and
138 * will not be handled in the fallback case because they are mostly
139 * needed for RTL languages which need more proper shaping support. */
140 if constexpr (!T::SUPPORTS_RTL) continue;
141
142 buff += T::AppendToBuffer(buff, buffer_last, c);
143 if (buff >= buffer_last) break;
144 continue;
145 }
146
147 FontIndex font_index = FontCache::GetFontIndexForCharacter(state.fontsize, c);
148
149 if (font_index == INVALID_FONT_INDEX) {
150 _missing_glyphs.Insert(state.fontsize, c);
151 font_index = FontCache::GetDefaultFontIndex(state.fontsize);
152 }
153
154 if (state.font_index == font_index) {
155 buff += T::AppendToBuffer(buff, buffer_last, c);
156 if (buff >= buffer_last) break;
157 continue;
158 }
159
160 /* This character goes in the next run so don't advance. */
161 state.font_index = font_index;
162
163 it = cur;
164 }
165
166 if (buff - buff_begin > 0 && (font_mapping.empty() || font_mapping.back().first != buff - buff_begin)) {
167 font_mapping.emplace_back(buff - buff_begin, f);
168 }
169 f = {state.font_index, state.cur_colour};
170 }
171
172 /* Better safe than sorry. */
173 *buff = '\0';
174
175 if (font_mapping.empty() || font_mapping.back().first != buff - buff_begin) {
176 font_mapping.emplace_back(buff - buff_begin, f);
177 }
178
179 if constexpr (!std::is_same_v<T, FallbackParagraphLayoutFactory>) {
180 /* Don't layout if all runs use a built-in font and we're not using the fallback layouter. */
181 if (std::ranges::all_of(font_mapping, [](const auto &i) { return i.second.GetFontCache().IsBuiltInFont(); })) {
182 return;
183 }
184 }
185
186 line.layout = T::GetParagraphLayout(buff_begin, buff, font_mapping);
187 line.state_after = state;
188}
189
196Layouter::Layouter(std::string_view str, int maxw, FontSize fontsize) : string(str)
197{
198 FontState state(TC_INVALID, fontsize, FontCache::GetDefaultFontIndex(fontsize));
199
200 while (true) {
201 auto line_length = str.find_first_of('\n');
202 auto str_line = str.substr(0, line_length);
203
204 LineCacheItem &line = GetCachedParagraphLayout(str_line, state);
205 if (line.layout != nullptr) {
206 state = line.state_after;
207 line.layout->Reflow();
208 } else {
209 /* Line is new, layout it */
210 FontState old_state = state;
211
212#if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ)
213 if (line.layout == nullptr) {
214 GetLayouter<ICUParagraphLayoutFactory>(line, str_line, state);
215 if (line.layout == nullptr) {
216 state = std::move(old_state);
217 }
218 }
219#endif
220
221#ifdef WITH_UNISCRIBE
222 if (line.layout == nullptr) {
223 GetLayouter<UniscribeParagraphLayoutFactory>(line, str_line, state);
224 if (line.layout == nullptr) {
225 state = std::move(old_state);
226 }
227 }
228#endif
229
230#ifdef WITH_COCOA
231 if (line.layout == nullptr) {
232 GetLayouter<CoreTextParagraphLayoutFactory>(line, str_line, state);
233 if (line.layout == nullptr) {
234 state = std::move(old_state);
235 }
236 }
237#endif
238
239 if (line.layout == nullptr) {
240 GetLayouter<FallbackParagraphLayoutFactory>(line, str_line, state);
241 }
242 }
243
244 if (line.cached_width != maxw) {
245 /* First run or width has changed, so we need to go through the layouter. Lines are moved into a cache to
246 * be reused if the width is not changed. */
247 line.cached_layout.clear();
248 line.cached_width = maxw;
249 for (;;) {
250 auto l = line.layout->NextLine(maxw);
251 if (l == nullptr) break;
252 line.cached_layout.push_back(std::move(l));
253 }
254 }
255
256 /* Retrieve layout from the cache. */
257 for (const auto &l : line.cached_layout) {
258 this->push_back(l.get());
259 }
260
261 /* Break out if this was the last line. */
262 if (line_length == std::string_view::npos) {
263 break;
264 }
265
266 /* Go to the next line. */
267 str.remove_prefix(line_length + 1);
268 }
269}
270
276{
277 Dimension d = { 0, 0 };
278 for (const auto &l : *this) {
279 d.width = std::max<uint>(d.width, l->GetWidth());
280 d.height += l->GetLeading();
281 }
282 return d;
283}
284
288static bool IsConsumedFormattingCode(char32_t ch)
289{
290 if (ch >= SCC_BLUE && ch <= SCC_BLACK) return true;
291 if (ch == SCC_PUSH_COLOUR) return true;
292 if (ch == SCC_POP_COLOUR) return true;
293 if (ch >= SCC_FIRST_FONT && ch <= SCC_LAST_FONT) return true;
294 /* All other characters defined in Unicode standard are assumed to be non-consumed. */
295 return false;
296}
297
304ParagraphLayouter::Position Layouter::GetCharPosition(std::string_view::const_iterator ch) const
305{
306 const auto &line = this->front();
307
308 /* Pointer to the end-of-string marker? Return total line width. */
309 if (ch == this->string.end()) {
310 Point p = {_current_text_dir == TD_LTR ? line->GetWidth() : 0, 0};
311 return p;
312 }
313
314 /* Initial position, returned if character not found. */
315 const ParagraphLayouter::Position initial_position = Point{_current_text_dir == TD_LTR ? 0 : line->GetWidth(), 0};
316
317 /* Find the code point index which corresponds to the char
318 * pointer into our UTF-8 source string. */
319 size_t index = 0;
320 {
321 Utf8View view(this->string);
322 const size_t offset = ch - this->string.begin();
323 const auto pos = view.GetIterAtByte(offset);
324
325 /* We couldn't find the code point index. */
326 if (pos.GetByteOffset() != offset) return initial_position;
327
328 for (auto it = view.begin(); it < pos; ++it) {
329 char32_t c = *it;
330 if (!IsConsumedFormattingCode(c)) index += line->GetInternalCharLength(c);
331 }
332 }
333
334 const ParagraphLayouter::Position *position = &initial_position;
335
336 /* Scan all runs until we've found our code point index. */
337 size_t best_index = SIZE_MAX;
338 for (int run_index = 0; run_index < line->CountRuns(); run_index++) {
339 const ParagraphLayouter::VisualRun &run = line->GetVisualRun(run_index);
340 const auto &positions = run.GetPositions();
341 const auto &charmap = run.GetGlyphToCharMap();
342
343 auto itp = positions.begin();
344 for (auto it = charmap.begin(); it != charmap.end(); ++it, ++itp) {
345 const size_t cur_index = static_cast<size_t>(*it);
346 /* Found exact character match? */
347 if (cur_index == index) return *itp;
348
349 /* If the character we are looking for has been combined with other characters to form a ligature then
350 * we may not be able to find an exact match. We don't actually know if our character is part of a
351 * ligature. In this case we will aim to select the first character of the ligature instead, so the best
352 * index is the index nearest to but lower than the desired index. */
353 if (cur_index < index && (best_index < cur_index || best_index == SIZE_MAX)) {
354 best_index = cur_index;
355 position = &*itp;
356 }
357 }
358 }
359
360 /* At the end of the run but still didn't find our character so probably a trailing ligature, use the last found position. */
361 return *position;
362}
363
370ptrdiff_t Layouter::GetCharAtPosition(int x, size_t line_index) const
371{
372 if (line_index >= this->size()) return -1;
373
374 const auto &line = this->at(line_index);
375
376 for (int run_index = 0; run_index < line->CountRuns(); run_index++) {
377 const ParagraphLayouter::VisualRun &run = line->GetVisualRun(run_index);
378 const auto &glyphs = run.GetGlyphs();
379 const auto &positions = run.GetPositions();
380 const auto &charmap = run.GetGlyphToCharMap();
381
382 for (int i = 0; i < run.GetGlyphCount(); i++) {
383 /* Not a valid glyph (empty). */
384 if (glyphs[i] == 0xFFFF) continue;
385
386 int begin_x = positions[i].left;
387 int end_x = positions[i].right + 1;
388
389 if (IsInsideMM(x, begin_x, end_x)) {
390 /* Found our glyph, now convert to UTF-8 string index. */
391 size_t index = charmap[i];
392
393 size_t cur_idx = 0;
394 Utf8View view(this->string);
395 for (auto it = view.begin(), end = view.end(); it != end; ++it) {
396 if (cur_idx == index) return it.GetByteOffset();
397
398 char32_t c = *it;
399 if (!IsConsumedFormattingCode(c)) cur_idx += line->GetInternalCharLength(c);
400 }
401 }
402 }
403 }
404
405 return -1;
406}
407
412{
413#if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ)
415#endif /* WITH_ICU_I18N && WITH_HARFBUZZ */
416}
417
421void Layouter::ResetFontCache([[maybe_unused]] FontSize size)
422{
423 /* We must reset the linecache since it references the just freed fonts */
425
426#if defined(WITH_UNISCRIBE)
427 UniscribeResetScriptCache(size);
428#endif
429#if defined(WITH_COCOA)
431#endif
432}
433
442{
443 if (linecache == nullptr) {
444 /* Create linecache on first access to avoid trouble with initialisation order of static variables. */
445 linecache = std::make_unique<LineCache>(4096);
446 }
447
448 if (auto match = linecache->GetIfValid(LineCacheQuery{state, str});
449 match != nullptr) {
450 return *match;
451 }
452
453 /* Create missing entry */
454 LineCacheKey key;
455 key.state_before = state;
456 key.str.assign(str);
457 linecache->Insert(key, {});
458 return *linecache->GetIfValid(key);
459}
460
465{
466 if (linecache != nullptr) linecache->Clear();
467}
468
477ParagraphLayouter::Position GetCharPosInString(std::string_view str, size_t pos, FontSize start_fontsize)
478{
479 assert(pos <= str.size());
480 auto it_ch = str.begin() + pos;
481
482 Layouter layout(str, INT32_MAX, start_fontsize);
483 return layout.GetCharPosition(it_ch);
484}
485
493ptrdiff_t GetCharAtPosition(std::string_view str, int x, FontSize start_fontsize)
494{
495 if (x < 0) return -1;
496
497 Layouter layout(str, INT32_MAX, start_fontsize);
498 return layout.GetCharAtPosition(x, 0);
499}
void UpdateAllVirtCoords()
Update the viewport coordinates of all signs.
static void LoadFontCaches(FontSizes fontsizes)
(Re)initialize the font cache related things, i.e.
static bool FindFallbackFont(const std::string &language_isocode, FontSizes fontsizes, class MissingGlyphSearcher *callback)
We would like to have a fallback font as the current one doesn't contain all characters we need.
Definition fontcache.cpp:58
Container with information about a font.
Definition gfx_layout.h:98
static void InitializeLayouter()
Initialize data needed for the ICU layouter.
The layouter performs all the layout work.
Definition gfx_layout.h:161
static void Initialize()
Perform initialization of layout engine.
Layouter(std::string_view str, int maxw=INT32_MAX, FontSize fontsize=FS_NORMAL)
Create a new layouter.
static void ResetFontCache(FontSize fs)
Reset cached font information.
ptrdiff_t GetCharAtPosition(int x, size_t line_index) const
Get the character that is at a pixel position in the first line of the layouted text.
ParagraphLayouter::Position GetCharPosition(std::string_view::const_iterator ch) const
Get the position of a character in the layout.
static std::unique_ptr< LineCache > linecache
Cache of ParagraphLayout lines.
Definition gfx_layout.h:205
static void ResetLineCache()
Clear line cache.
Dimension GetBounds()
Get the boundaries of this paragraph.
static LineCacheItem & GetCachedParagraphLayout(std::string_view str, const FontState &state)
Get reference to cache item.
A searcher for missing glyphs.
FontSizes fontsizes
Font sizes to search for.
Position of a glyph within a VisualRun.
Definition gfx_layout.h:117
Visual run contains data about the bit of text with the same font.
Definition gfx_layout.h:130
std::set< char32_t > GetRequiredGlyphs(FontSizes fontsizes) override
Get set of glyphs required for the current language.
A timeout timer will fire once after the interval.
Definition timer.h:116
void Reset()
Reset the timer, so it will fire again after the timeout.
Definition timer.h:140
Constant span of UTF-8 encoded data.
Definition utf8.hpp:30
iterator GetIterAtByte(size_t offset) const
Create iterator pointing at codepoint, which occupies the byte position "offset".
Definition utf8.cpp:83
Control codes that are embedded in the translation strings.
Functions related to debugging.
void LoadStringWidthTable(FontSizes fontsizes)
Initialize _stringwidth_table cache for the specified font sizes.
Definition gfx.cpp:1250
Functions related to the gfx engine.
ParagraphLayouter::Position GetCharPosInString(std::string_view str, size_t pos, FontSize start_fontsize)
Get the leading corner of a character in a single-line string relative to the start of the string.
static bool IsConsumedFormattingCode(char32_t ch)
Test whether a character is a non-printable formatting code.
static void GetLayouter(Layouter::LineCacheItem &line, std::string_view str, FontState &state)
Helper for getting a ParagraphLayouter of the given type.
ptrdiff_t GetCharAtPosition(std::string_view str, int x, FontSize start_fontsize)
Get the character from a string that is drawn at a specific position.
Functions related to laying out the texts.
std::vector< std::pair< int, Font > > FontMap
Mapping from index to font.
Definition gfx_layout.h:107
Functions related to laying out the texts as fallback.
Functions related to laying out the texts with ICU.
FontSize
Available font sizes.
Definition gfx_type.h:248
@ FS_BEGIN
First font.
Definition gfx_type.h:255
constexpr FontSizes FONTSIZES_ALL
Mask of all possible font sizes.
Definition gfx_type.h:262
TextColour
Colour of the strings, see _string_colourmap in table/string_colours.h or docs/ottd-colourtext-palett...
Definition gfx_type.h:307
Integer math functions.
constexpr bool IsInsideMM(const size_t x, const size_t min, const size_t max) noexcept
Checks if a value is in an interval.
A number of safeguards to prevent using unsafe methods.
Definition of base types and functions in a cross-platform compatible way.
Functions related to low-level strings.
bool IsTextDirectionChar(char32_t c)
Is the given character a text direction character.
void MacOSResetScriptCache(FontSize size)
Delete CoreText font reference for a specific font size.
Functions related to localized text support on OSX.
Functions related to laying out text on Win32.
TextDirection _current_text_dir
Text direction of the currently selected language.
Definition strings.cpp:56
Functions related to OTTD's strings.
@ TD_LTR
Text is written left-to-right by default.
Dimensions (a width and height) of a rectangle in 2D.
Text drawing parameters, which can change while drawing a line, but are kept between multiple parts o...
Definition gfx_layout.h:22
void PushColour()
Push the current colour on to the stack.
Definition gfx_layout.h:57
void SetColour(TextColour c)
Switch to new colour c.
Definition gfx_layout.h:37
void PopColour()
Switch to and pop the last saved colour on the stack.
Definition gfx_layout.h:47
void SetFontSize(FontSize f)
Switch to using a new font f.
Definition gfx_layout.h:66
FontSize fontsize
Current font size.
Definition gfx_layout.h:23
FontIndex font_index
Current font index.
Definition gfx_layout.h:24
TextColour cur_colour
Current text colour.
Definition gfx_layout.h:25
Item in the linecache.
Definition gfx_layout.h:190
FontMap runs
Accessed by our ParagraphLayout::nextLine.
Definition gfx_layout.h:195
Buffer buffer
Accessed by our ParagraphLayout::nextLine.
Definition gfx_layout.h:194
int cached_width
Width used for the cached layout.
Definition gfx_layout.h:201
std::vector< std::unique_ptr< const ParagraphLayouter::Line > > cached_layout
Cached results of line layouting.
Definition gfx_layout.h:200
FontState state_after
Font state after the line.
Definition gfx_layout.h:197
std::unique_ptr< ParagraphLayouter > layout
Layout of the line.
Definition gfx_layout.h:198
Key into the linecache.
Definition gfx_layout.h:165
FontState state_before
Font state at the beginning of the line.
Definition gfx_layout.h:166
std::string str
Source string of the line (including colour and font size codes).
Definition gfx_layout.h:167
Definition of Interval and OneShot timers.
Definition of the Window system.
Handling of UTF-8 encoded data.
Functions related to (drawing on) viewports.
void ReInitAllWindows(bool zoom_changed)
Re-initialize all windows.
Definition window.cpp:3410
Window functions not directly related to making/drawing windows.