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"
16 #include "table/control_codes.h"
18 #include "gfx_layout_fallback.h"
20 #if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ)
21 #include "gfx_layout_icu.h"
22 #endif /* WITH_ICU_I18N && WITH_HARFBUZZ */
25 #include "os/windows/string_uniscribe.h"
26 #endif /* WITH_UNISCRIBE */
29 #include "os/macosx/string_osx.h"
32 #include "safeguards.h"
35 /** Cache of ParagraphLayout lines. */
36 Layouter::LineCache
*Layouter::linecache
;
38 /** Cache of Font instances. */
39 Layouter::FontColourMap
Layouter::fonts
[FS_END
];
43 * Construct a new font.
44 * @param size The font size to use for this font.
45 * @param colour The colour to draw this font in.
47 Font::Font(FontSize size
, TextColour colour
) :
48 fc(FontCache::Get(size
)), colour(colour
)
50 assert(size
< FS_END
);
54 * Helper for getting a ParagraphLayouter of the given type.
56 * @note In case no ParagraphLayouter could be constructed, line.layout will be nullptr.
57 * @param line The cache item to store our layouter in.
58 * @param str The string to create a layouter for.
59 * @param state The state of the font and color.
60 * @tparam T The type of layouter we want.
63 static inline void GetLayouter(Layouter::LineCacheItem
&line
, std::string_view str
, FontState
&state
)
65 if (line
.buffer
!= nullptr) free(line
.buffer
);
67 typename
T::CharType
*buff_begin
= MallocT
<typename
T::CharType
>(str
.size() + 1);
68 const typename
T::CharType
*buffer_last
= buff_begin
+ str
.size() + 1;
69 typename
T::CharType
*buff
= buff_begin
;
70 FontMap
&fontMapping
= line
.runs
;
71 Font
*f
= Layouter::GetFont(state
.fontsize
, state
.cur_colour
);
73 line
.buffer
= buff_begin
;
76 auto cur
= str
.begin();
79 * Go through the whole string while adding Font instances to the font map
80 * whenever the font changes, and convert the wide characters into a format
81 * usable by ParagraphLayout.
83 for (; buff
< buffer_last
&& cur
!= str
.end();) {
84 char32_t c
= Utf8Consume(cur
);
85 if (c
== '\0' || c
== '\n') {
86 /* Caller should already have filtered out these characters. */
88 } else if (c
>= SCC_BLUE
&& c
<= SCC_BLACK
) {
89 state
.SetColour((TextColour
)(c
- SCC_BLUE
));
90 } else if (c
== SCC_PUSH_COLOUR
) {
92 } else if (c
== SCC_POP_COLOUR
) {
94 } else if (c
>= SCC_FIRST_FONT
&& c
<= SCC_LAST_FONT
) {
95 state
.SetFontSize((FontSize
)(c
- SCC_FIRST_FONT
));
97 /* Filter out non printable characters */
98 if (!IsPrintable(c
)) continue;
99 /* Filter out text direction characters that shouldn't be drawn, and
100 * will not be handled in the fallback case because they are mostly
101 * needed for RTL languages which need more proper shaping support. */
102 if (!T::SUPPORTS_RTL
&& IsTextDirectionChar(c
)) continue;
103 buff
+= T::AppendToBuffer(buff
, buffer_last
, c
);
107 if (fontMapping
.count(buff
- buff_begin
) == 0) {
108 fontMapping
[buff
- buff_begin
] = f
;
110 f
= Layouter::GetFont(state
.fontsize
, state
.cur_colour
);
113 /* Better safe than sorry. */
116 if (fontMapping
.count(buff
- buff_begin
) == 0) {
117 fontMapping
[buff
- buff_begin
] = f
;
119 line
.layout
= T::GetParagraphLayout(buff_begin
, buff
, fontMapping
);
120 line
.state_after
= state
;
124 * Create a new layouter.
125 * @param str The string to create the layout for.
126 * @param maxw The maximum width.
127 * @param colour The colour of the font.
128 * @param fontsize The size of font to use.
130 Layouter::Layouter(std::string_view str
, int maxw
, TextColour colour
, FontSize fontsize
) : string(str
)
132 FontState
state(colour
, 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 Point
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
= { line
->GetWidth(), 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 static const std::vector
<Point
> zero
= { {0, 0} };
249 auto position
= zero
.begin();
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 for (int run_index
= 0; run_index
< line
->CountRuns(); run_index
++) {
258 const ParagraphLayouter::VisualRun
&run
= line
->GetVisualRun(run_index
);
259 const auto &positions
= run
.GetPositions();
260 const auto &charmap
= run
.GetGlyphToCharMap();
262 /* Run starts after our character, use the last found position. */
263 if ((size_t)charmap
.front() > index
) return *position
;
265 position
= positions
.begin();
266 for (auto it
= charmap
.begin(); it
!= charmap
.end(); /* nothing */) {
267 /* Plain honest-to-$deity match. */
268 if ((size_t)*it
== index
) return *position
;
270 if (it
== charmap
.end()) break;
272 /* We just passed our character, it's probably a ligature, use the last found position. */
273 if ((size_t)*it
> index
) return *position
;
278 /* At the end of the run but still didn't find our character so probably a trailing ligature, use the last found position. */
283 * Get the character that is at a pixel position in the first line of the layouted text.
284 * @param x Position in the string.
285 * @param line_index Which line of the layout to search
286 * @return String offset of the position (bytes) or -1 if no character is at the position.
288 ptrdiff_t Layouter::GetCharAtPosition(int x
, size_t line_index
) const
290 if (line_index
>= this->size()) return -1;
292 const auto &line
= this->at(line_index
);
294 for (int run_index
= 0; run_index
< line
->CountRuns(); run_index
++) {
295 const ParagraphLayouter::VisualRun
&run
= line
->GetVisualRun(run_index
);
296 const auto &glyphs
= run
.GetGlyphs();
297 const auto &positions
= run
.GetPositions();
298 const auto &charmap
= run
.GetGlyphToCharMap();
300 for (int i
= 0; i
< run
.GetGlyphCount(); i
++) {
301 /* Not a valid glyph (empty). */
302 if (glyphs
[i
] == 0xFFFF) continue;
304 int begin_x
= positions
[i
].x
;
305 int end_x
= positions
[i
+ 1].x
;
307 if (IsInsideMM(x
, begin_x
, end_x
)) {
308 /* Found our glyph, now convert to UTF-8 string index. */
309 size_t index
= charmap
[i
];
312 for (auto str
= this->string
.begin(); str
!= this->string
.end();) {
313 if (cur_idx
== index
) return str
- this->string
.begin();
315 char32_t c
= Utf8Consume(str
);
316 if (!IsConsumedFormattingCode(c
)) cur_idx
+= line
->GetInternalCharLength(c
);
326 * Get a static font instance.
328 Font
*Layouter::GetFont(FontSize size
, TextColour colour
)
330 FontColourMap::iterator it
= fonts
[size
].find(colour
);
331 if (it
!= fonts
[size
].end()) return it
->second
.get();
333 fonts
[size
][colour
] = std::make_unique
<Font
>(size
, colour
);
334 return fonts
[size
][colour
].get();
338 * Perform initialization of layout engine.
340 void Layouter::Initialize()
342 #if defined(WITH_ICU_I18N) && defined(WITH_HARFBUZZ)
343 ICUParagraphLayoutFactory::InitializeLayouter();
344 #endif /* WITH_ICU_I18N && WITH_HARFBUZZ */
348 * Reset cached font information.
349 * @param size Font size to reset.
351 void Layouter::ResetFontCache(FontSize size
)
355 /* We must reset the linecache since it references the just freed fonts */
358 #if defined(WITH_UNISCRIBE)
359 UniscribeResetScriptCache(size
);
361 #if defined(WITH_COCOA)
362 MacOSResetScriptCache(size
);
367 * Get reference to cache item.
368 * If the item does not exist yet, it is default constructed.
369 * @param str Source string of the line (including colour and font size codes).
370 * @param state State of the font at the beginning of the line.
371 * @return Reference to cache item.
373 Layouter::LineCacheItem
&Layouter::GetCachedParagraphLayout(std::string_view str
, const FontState
&state
)
375 if (linecache
== nullptr) {
376 /* Create linecache on first access to avoid trouble with initialisation order of static variables. */
377 linecache
= new LineCache();
380 if (auto match
= linecache
->find(LineCacheQuery
{state
, str
});
381 match
!= linecache
->end()) {
382 return match
->second
;
385 /* Create missing entry */
387 key
.state_before
= state
;
389 return (*linecache
)[key
];
395 void Layouter::ResetLineCache()
397 if (linecache
!= nullptr) linecache
->clear();
401 * Reduce the size of linecache if necessary to prevent infinite growth.
403 void Layouter::ReduceLineCache()
405 if (linecache
!= nullptr) {
406 /* TODO LRU cache would be fancy, but not exactly necessary */
407 if (linecache
->size() > 4096) ResetLineCache();