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 <http://www.gnu.org/licenses/>.
8 /** @file gfx_layout.cpp Handling of laying out text. */
11 #include "core/math_func.hpp"
12 #include "gfx_layout.h"
13 #include "string_func.h"
14 #include "strings_func.h"
17 #include "table/control_codes.h"
19 #include "gfx_layout_fallback.h"
21 #if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ)
22 #include "gfx_layout_icu.h"
23 #endif /* WITH_ICU_I18N && WITH_HARFBUZZ */
26 #include "os/windows/string_uniscribe.h"
27 #endif /* WITH_UNISCRIBE */
30 #include "os/macosx/string_osx.h"
33 #include "safeguards.h"
36 /** Cache of ParagraphLayout lines. */
37 Layouter::LineCache
*Layouter::linecache
;
39 /** Cache of Font instances. */
40 Layouter::FontColourMap
Layouter::fonts
[FS_END
];
44 * Construct a new font.
45 * @param size The font size to use for this font.
46 * @param colour The colour to draw this font in.
48 Font::Font(FontSize size
, TextColour colour
) :
49 fc(FontCache::Get(size
)), colour(colour
)
51 assert(size
< FS_END
);
55 * Helper for getting a ParagraphLayouter of the given type.
57 * @note In case no ParagraphLayouter could be constructed, line.layout will be nullptr.
58 * @param line The cache item to store our layouter in.
59 * @param str The string to create a layouter for.
60 * @param state The state of the font and color.
61 * @tparam T The type of layouter we want.
64 static inline void GetLayouter(Layouter::LineCacheItem
&line
, std::string_view str
, FontState
&state
)
66 if (line
.buffer
!= nullptr) free(line
.buffer
);
68 typename
T::CharType
*buff_begin
= MallocT
<typename
T::CharType
>(str
.size() + 1);
69 const typename
T::CharType
*buffer_last
= buff_begin
+ str
.size() + 1;
70 typename
T::CharType
*buff
= buff_begin
;
71 FontMap
&fontMapping
= line
.runs
;
72 Font
*f
= Layouter::GetFont(state
.fontsize
, state
.cur_colour
);
74 line
.buffer
= buff_begin
;
77 auto cur
= str
.begin();
80 * Go through the whole string while adding Font instances to the font map
81 * whenever the font changes, and convert the wide characters into a format
82 * usable by ParagraphLayout.
84 for (; buff
< buffer_last
&& cur
!= str
.end();) {
85 char32_t c
= Utf8Consume(cur
);
86 if (c
== '\0' || c
== '\n') {
87 /* Caller should already have filtered out these characters. */
89 } else if (c
>= SCC_BLUE
&& c
<= SCC_BLACK
) {
90 state
.SetColour((TextColour
)(c
- SCC_BLUE
));
91 } else if (c
== SCC_PUSH_COLOUR
) {
93 } else if (c
== SCC_POP_COLOUR
) {
95 } else if (c
>= SCC_FIRST_FONT
&& c
<= SCC_LAST_FONT
) {
96 state
.SetFontSize((FontSize
)(c
- SCC_FIRST_FONT
));
98 /* Filter out non printable characters */
99 if (!IsPrintable(c
)) continue;
100 /* Filter out text direction characters that shouldn't be drawn, and
101 * will not be handled in the fallback case because they are mostly
102 * needed for RTL languages which need more proper shaping support. */
103 if (!T::SUPPORTS_RTL
&& IsTextDirectionChar(c
)) continue;
104 buff
+= T::AppendToBuffer(buff
, buffer_last
, c
);
108 if (fontMapping
.count(buff
- buff_begin
) == 0) {
109 fontMapping
[buff
- buff_begin
] = f
;
111 f
= Layouter::GetFont(state
.fontsize
, state
.cur_colour
);
114 /* Better safe than sorry. */
117 if (fontMapping
.count(buff
- buff_begin
) == 0) {
118 fontMapping
[buff
- buff_begin
] = f
;
120 line
.layout
= T::GetParagraphLayout(buff_begin
, buff
, fontMapping
);
121 line
.state_after
= state
;
125 * Create a new layouter.
126 * @param str The string to create the layout for.
127 * @param maxw The maximum width.
128 * @param fontsize The size of font to use.
130 Layouter::Layouter(std::string_view str
, int maxw
, FontSize fontsize
) : string(str
)
132 FontState
state(TC_INVALID
, fontsize
);
135 auto line_length
= str
.find_first_of('\n');
136 auto str_line
= str
.substr(0, line_length
);
138 LineCacheItem
&line
= GetCachedParagraphLayout(str_line
, state
);
139 if (line
.layout
!= nullptr) {
140 state
= line
.state_after
;
141 line
.layout
->Reflow();
143 /* Line is new, layout it */
144 FontState old_state
= state
;
146 #if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ)
147 if (line
.layout
== nullptr) {
148 GetLayouter
<ICUParagraphLayoutFactory
>(line
, str_line
, state
);
149 if (line
.layout
== nullptr) {
155 #ifdef WITH_UNISCRIBE
156 if (line
.layout
== nullptr) {
157 GetLayouter
<UniscribeParagraphLayoutFactory
>(line
, str_line
, state
);
158 if (line
.layout
== nullptr) {
165 if (line
.layout
== nullptr) {
166 GetLayouter
<CoreTextParagraphLayoutFactory
>(line
, str_line
, state
);
167 if (line
.layout
== nullptr) {
173 if (line
.layout
== nullptr) {
174 GetLayouter
<FallbackParagraphLayoutFactory
>(line
, str_line
, state
);
178 /* Move all lines into a local cache so we can reuse them later on more easily. */
180 auto l
= line
.layout
->NextLine(maxw
);
181 if (l
== nullptr) break;
182 this->push_back(std::move(l
));
185 /* Break out if this was the last line. */
186 if (line_length
== std::string_view::npos
) {
190 /* Go to the next line. */
191 str
.remove_prefix(line_length
+ 1);
196 * Get the boundaries of this paragraph.
197 * @return The boundaries.
199 Dimension
Layouter::GetBounds()
201 Dimension d
= { 0, 0 };
202 for (const auto &l
: *this) {
203 d
.width
= std::max
<uint
>(d
.width
, l
->GetWidth());
204 d
.height
+= l
->GetLeading();
210 * Test whether a character is a non-printable formatting code
212 static bool IsConsumedFormattingCode(char32_t ch
)
214 if (ch
>= SCC_BLUE
&& ch
<= SCC_BLACK
) return true;
215 if (ch
== SCC_PUSH_COLOUR
) return true;
216 if (ch
== SCC_POP_COLOUR
) return true;
217 if (ch
>= SCC_FIRST_FONT
&& ch
<= SCC_LAST_FONT
) return true;
218 // All other characters defined in Unicode standard are assumed to be non-consumed.
223 * Get the position of a character in the layout.
224 * @param ch Character to get the position of. Must be an iterator of the string passed to the constructor.
225 * @return Upper left corner of the character relative to the start of the string.
226 * @note Will only work right for single-line strings.
228 ParagraphLayouter::Position
Layouter::GetCharPosition(std::string_view::const_iterator ch
) const
230 const auto &line
= this->front();
232 /* Pointer to the end-of-string marker? Return total line width. */
233 if (ch
== this->string
.end()) {
234 Point p
= {_current_text_dir
== TD_LTR
? line
->GetWidth() : 0, 0};
238 /* Find the code point index which corresponds to the char
239 * pointer into our UTF-8 source string. */
241 auto str
= this->string
.begin();
243 char32_t c
= Utf8Consume(str
);
244 if (!IsConsumedFormattingCode(c
)) index
+= line
->GetInternalCharLength(c
);
247 /* Initial position, returned if character not found. */
248 const ParagraphLayouter::Position initial_position
= Point
{_current_text_dir
== TD_LTR
? 0 : line
->GetWidth(), 0};
249 const ParagraphLayouter::Position
*position
= &initial_position
;
251 /* We couldn't find the code point index. */
252 if (str
!= ch
) return *position
;
254 /* Valid character. */
256 /* Scan all runs until we've found our code point index. */
257 size_t best_index
= SIZE_MAX
;
258 for (int run_index
= 0; run_index
< line
->CountRuns(); run_index
++) {
259 const ParagraphLayouter::VisualRun
&run
= line
->GetVisualRun(run_index
);
260 const auto &positions
= run
.GetPositions();
261 const auto &charmap
= run
.GetGlyphToCharMap();
263 auto itp
= positions
.begin();
264 for (auto it
= charmap
.begin(); it
!= charmap
.end(); ++it
, ++itp
) {
265 const size_t cur_index
= static_cast<size_t>(*it
);
266 /* Found exact character match? */
267 if (cur_index
== index
) return *itp
;
269 /* If the character we are looking for has been combined with other characters to form a ligature then
270 * we may not be able to find an exact match. We don't actually know if our character is part of a
271 * ligature. In this case we will aim to select the first character of the ligature instead, so the best
272 * index is the index nearest to but lower than the desired index. */
273 if (cur_index
< index
&& (best_index
< cur_index
|| best_index
== SIZE_MAX
)) {
274 best_index
= cur_index
;
280 /* At the end of the run but still didn't find our character so probably a trailing ligature, use the last found position. */
285 * Get the character that is at a pixel position in the first line of the layouted text.
286 * @param x Position in the string.
287 * @param line_index Which line of the layout to search
288 * @return String offset of the position (bytes) or -1 if no character is at the position.
290 ptrdiff_t Layouter::GetCharAtPosition(int x
, size_t line_index
) const
292 if (line_index
>= this->size()) return -1;
294 const auto &line
= this->at(line_index
);
296 for (int run_index
= 0; run_index
< line
->CountRuns(); run_index
++) {
297 const ParagraphLayouter::VisualRun
&run
= line
->GetVisualRun(run_index
);
298 const auto &glyphs
= run
.GetGlyphs();
299 const auto &positions
= run
.GetPositions();
300 const auto &charmap
= run
.GetGlyphToCharMap();
302 for (int i
= 0; i
< run
.GetGlyphCount(); i
++) {
303 /* Not a valid glyph (empty). */
304 if (glyphs
[i
] == 0xFFFF) continue;
306 int begin_x
= positions
[i
].left
;
307 int end_x
= positions
[i
].right
+ 1;
309 if (IsInsideMM(x
, begin_x
, end_x
)) {
310 /* Found our glyph, now convert to UTF-8 string index. */
311 size_t index
= charmap
[i
];
314 for (auto str
= this->string
.begin(); str
!= this->string
.end();) {
315 if (cur_idx
== index
) return str
- this->string
.begin();
317 char32_t c
= Utf8Consume(str
);
318 if (!IsConsumedFormattingCode(c
)) cur_idx
+= line
->GetInternalCharLength(c
);
328 * Get a static font instance.
330 Font
*Layouter::GetFont(FontSize size
, TextColour colour
)
332 FontColourMap::iterator it
= fonts
[size
].find(colour
);
333 if (it
!= fonts
[size
].end()) return it
->second
.get();
335 fonts
[size
][colour
] = std::make_unique
<Font
>(size
, colour
);
336 return fonts
[size
][colour
].get();
340 * Perform initialization of layout engine.
342 void Layouter::Initialize()
344 #if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ)
345 ICUParagraphLayoutFactory::InitializeLayouter();
346 #endif /* WITH_ICU_I18N && WITH_HARFBUZZ */
350 * Reset cached font information.
351 * @param size Font size to reset.
353 void Layouter::ResetFontCache(FontSize size
)
357 /* We must reset the linecache since it references the just freed fonts */
360 #if defined(WITH_UNISCRIBE)
361 UniscribeResetScriptCache(size
);
363 #if defined(WITH_COCOA)
364 MacOSResetScriptCache(size
);
369 * Get reference to cache item.
370 * If the item does not exist yet, it is default constructed.
371 * @param str Source string of the line (including colour and font size codes).
372 * @param state State of the font at the beginning of the line.
373 * @return Reference to cache item.
375 Layouter::LineCacheItem
&Layouter::GetCachedParagraphLayout(std::string_view str
, const FontState
&state
)
377 if (linecache
== nullptr) {
378 /* Create linecache on first access to avoid trouble with initialisation order of static variables. */
379 linecache
= new LineCache();
382 if (auto match
= linecache
->find(LineCacheQuery
{state
, str
});
383 match
!= linecache
->end()) {
384 return match
->second
;
387 /* Create missing entry */
389 key
.state_before
= state
;
391 return (*linecache
)[std::move(key
)];
397 void Layouter::ResetLineCache()
399 if (linecache
!= nullptr) linecache
->clear();
403 * Reduce the size of linecache if necessary to prevent infinite growth.
405 void Layouter::ReduceLineCache()
407 if (linecache
!= nullptr) {
408 /* TODO LRU cache would be fancy, but not exactly necessary */
409 if (linecache
->size() > 4096) ResetLineCache();
414 * Get the leading corner of a character in a single-line string relative
415 * to the start of the string.
416 * @param str String containing the character.
417 * @param ch Pointer to the character in the string.
418 * @param start_fontsize Font size to start the text with.
419 * @return Upper left corner of the glyph associated with the character.
421 ParagraphLayouter::Position
GetCharPosInString(std::string_view str
, const char *ch
, FontSize start_fontsize
)
423 /* Ensure "ch" is inside "str" or at the exact end. */
424 assert(ch
>= str
.data() && (ch
- str
.data()) <= static_cast<ptrdiff_t>(str
.size()));
425 auto it_ch
= str
.begin() + (ch
- str
.data());
427 Layouter
layout(str
, INT32_MAX
, start_fontsize
);
428 return layout
.GetCharPosition(it_ch
);
432 * Get the character from a string that is drawn at a specific position.
433 * @param str String to test.
434 * @param x Position relative to the start of the string.
435 * @param start_fontsize Font size to start the text with.
436 * @return Index of the character position or -1 if there is no character at the position.
438 ptrdiff_t GetCharAtPosition(std::string_view str
, int x
, FontSize start_fontsize
)
440 if (x
< 0) return -1;
442 Layouter
layout(str
, INT32_MAX
, start_fontsize
);
443 return layout
.GetCharAtPosition(x
, 0);