OpenTTD Source 20260311-master-g511d3794ce
string_osx.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
9
10#include "../../stdafx.h"
11#include "string_osx.h"
12#include "../../string_func.h"
13#include "../../strings_func.h"
14#include "../../core/utf8.hpp"
16#include "../../fontcache.h"
17#include "../../zoom_func.h"
18#include "macos.h"
19
20#include <CoreFoundation/CoreFoundation.h>
21#include "../../safeguards.h"
22
23
28
29
33class CoreTextParagraphLayout : public ParagraphLayouter {
34private:
36 ptrdiff_t length;
37 const FontMap &font_map;
38
40
41 CFIndex cur_offset = 0;
42
43public:
45 class CoreTextVisualRun : public ParagraphLayouter::VisualRun {
46 private:
47 std::vector<GlyphID> glyphs;
48 std::vector<Position> positions;
49 std::vector<int> glyph_to_char;
50
51 int total_advance = 0;
52 Font *font;
53
54 public:
55 CoreTextVisualRun(CTRunRef run, Font *font, const CoreTextParagraphLayoutFactory::CharType *buff);
56 CoreTextVisualRun(CoreTextVisualRun &&other) = default;
57
58 std::span<const GlyphID> GetGlyphs() const override { return this->glyphs; }
59 std::span<const Position> GetPositions() const override { return this->positions; }
60 std::span<const int> GetGlyphToCharMap() const override { return this->glyph_to_char; }
61
62 const Font *GetFont() const override { return this->font; }
63 int GetLeading() const override { return this->font->fc->GetHeight(); }
64 size_t GetGlyphCount() const override { return this->glyphs.size(); }
65 int GetAdvance() const { return this->total_advance; }
66 };
67
69 class CoreTextLine : public std::vector<CoreTextVisualRun>, public ParagraphLayouter::Line {
70 public:
71 CoreTextLine(CFAutoRelease<CTLineRef> line, const FontMap &font_mapping, const CoreTextParagraphLayoutFactory::CharType *buff)
72 {
73 CFArrayRef runs = CTLineGetGlyphRuns(line.get());
74 for (CFIndex i = 0; i < CFArrayGetCount(runs); i++) {
75 CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, i);
76
77 /* Extract font information for this run. */
78 CFRange chars = CTRunGetStringRange(run);
79 auto map = std::ranges::upper_bound(font_mapping, chars.location, std::less{}, &std::pair<int, Font *>::first);
80
81 this->emplace_back(run, map->second, buff);
82 }
83 }
84
85 int GetLeading() const override;
86 int GetWidth() const override;
87 size_t CountRuns() const override { return this->size(); }
88 const VisualRun &GetVisualRun(size_t run) const override { return this->at(run); }
89
90 int GetInternalCharLength(char32_t c) const override
91 {
92 /* CoreText uses UTF-16 internally which means we need to account for surrogate pairs. */
93 return c >= 0x010000U ? 2 : 1;
94 }
95 };
96
97 CoreTextParagraphLayout(CFAutoRelease<CTTypesetterRef> typesetter, const CoreTextParagraphLayoutFactory::CharType *buffer, ptrdiff_t len, const FontMap &font_mapping) : text_buffer(buffer), length(len), font_map(font_mapping), typesetter(std::move(typesetter))
98 {
99 this->Reflow();
100 }
101
102 void Reflow() override
103 {
104 this->cur_offset = 0;
105 }
106
107 std::unique_ptr<const Line> NextLine(int max_width) override;
108};
109
110
116static CGFloat SpriteFontGetWidth(void *ref_con)
117{
118 FontSize fs = (FontSize)((size_t)ref_con >> 24);
119 char32_t c = (char32_t)((size_t)ref_con & 0xFFFFFF);
120
121 return GetGlyphWidth(fs, c);
122}
123
124static const CTRunDelegateCallbacks _sprite_font_callback = {
125 kCTRunDelegateCurrentVersion, nullptr, nullptr, nullptr,
127};
128
129/* static */ std::unique_ptr<ParagraphLayouter> CoreTextParagraphLayoutFactory::GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &font_mapping)
130{
131 /* Can't layout an empty string. */
132 ptrdiff_t length = buff_end - buff;
133 if (length == 0) return nullptr;
134
135 /* Can't layout our in-built sprite fonts. */
136 for (const auto &[position, font] : font_mapping) {
137 if (font->fc->IsBuiltInFont()) return nullptr;
138 }
139
140 /* Make attributed string with embedded font information. */
141 CFAutoRelease<CFMutableAttributedStringRef> str(CFAttributedStringCreateMutable(kCFAllocatorDefault, 0));
142 CFAttributedStringBeginEditing(str.get());
143
144 CFAutoRelease<CFStringRef> base(CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, buff, length, kCFAllocatorNull));
145 CFAttributedStringReplaceString(str.get(), CFRangeMake(0, 0), base.get());
146
147 const UniChar replacement_char = 0xFFFC;
148 CFAutoRelease<CFStringRef> replacement_str(CFStringCreateWithCharacters(kCFAllocatorDefault, &replacement_char, 1));
149
150 /* Apply font and colour ranges to our string. This is important to make sure
151 * that we get proper glyph boundaries on style changes. */
152 int last = 0;
153 for (const auto &[position, font] : font_mapping) {
154 if (position - last == 0) continue;
155
156 CTFontRef font_handle = static_cast<CTFontRef>(font->fc->GetOSHandle());
157 if (font_handle == nullptr) {
158 if (!_font_cache[font->fc->GetSize()]) {
159 /* Cache font information. */
160 CFAutoRelease<CFStringRef> font_name(CFStringCreateWithCString(kCFAllocatorDefault, font->fc->GetFontName().c_str(), kCFStringEncodingUTF8));
161 _font_cache[font->fc->GetSize()].reset(CTFontCreateWithName(font_name.get(), font->fc->GetFontSize(), nullptr));
162 }
163 font_handle = _font_cache[font->fc->GetSize()].get();
164 }
165 CFAttributedStringSetAttribute(str.get(), CFRangeMake(last, position - last), kCTFontAttributeName, font_handle);
166
167 CGColorRef colour = CGColorCreateGenericGray((uint8_t)font->colour / 255.0f, 1.0f); // We don't care about the real colours, just that they are different.
168 CFAttributedStringSetAttribute(str.get(), CFRangeMake(last, position - last), kCTForegroundColorAttributeName, colour);
169 CGColorRelease(colour);
170
171 /* Install a size callback for our special private-use sprite glyphs in case the font does not provide them. */
172 for (ssize_t c = last; c < position; c++) {
173 if (buff[c] >= SCC_SPRITE_START && buff[c] <= SCC_SPRITE_END && font->fc->MapCharToGlyph(buff[c], false) == 0) {
174 CFAutoRelease<CTRunDelegateRef> del(CTRunDelegateCreate(&_sprite_font_callback, (void *)(size_t)(buff[c] | (font->fc->GetSize() << 24))));
175 /* According to the official documentation, if a run delegate is used, the char should always be 0xFFFC. */
176 CFAttributedStringReplaceString(str.get(), CFRangeMake(c, 1), replacement_str.get());
177 CFAttributedStringSetAttribute(str.get(), CFRangeMake(c, 1), kCTRunDelegateAttributeName, del.get());
178 }
179 }
180
181 last = position;
182 }
183 CFAttributedStringEndEditing(str.get());
184
185 /* Create and return typesetter for the string. */
186 CFAutoRelease<CTTypesetterRef> typesetter(CTTypesetterCreateWithAttributedString(str.get()));
187
188 return typesetter ? std::make_unique<CoreTextParagraphLayout>(std::move(typesetter), buff, length, font_mapping) : nullptr;
189}
190
191/* virtual */ std::unique_ptr<const ParagraphLayouter::Line> CoreTextParagraphLayout::NextLine(int max_width)
192{
193 if (this->cur_offset >= this->length) return nullptr;
194
195 /* Get line break position, trying word breaking first and breaking somewhere if that doesn't work. */
196 CFIndex len = CTTypesetterSuggestLineBreak(this->typesetter.get(), this->cur_offset, max_width);
197 if (len <= 0) len = CTTypesetterSuggestClusterBreak(this->typesetter.get(), this->cur_offset, max_width);
198
199 /* Create line. */
200 CFAutoRelease<CTLineRef> line(CTTypesetterCreateLine(this->typesetter.get(), CFRangeMake(this->cur_offset, len)));
201 this->cur_offset += len;
202
203 if (!line) return nullptr;
204 return std::make_unique<CoreTextLine>(std::move(line), this->font_map, this->text_buffer);
205}
206
207CoreTextParagraphLayout::CoreTextVisualRun::CoreTextVisualRun(CTRunRef run, Font *font, const CoreTextParagraphLayoutFactory::CharType *buff) : font(font)
208{
209 this->glyphs.resize(CTRunGetGlyphCount(run));
210
211 /* Query map of glyphs to source string index. */
212 auto map = std::make_unique<CFIndex[]>(this->glyphs.size());
213 CTRunGetStringIndices(run, CFRangeMake(0, 0), map.get());
214
215 this->glyph_to_char.resize(this->glyphs.size());
216 for (size_t i = 0; i < this->glyph_to_char.size(); i++) this->glyph_to_char[i] = (int)map[i];
217
218 auto pts = std::make_unique<CGPoint[]>(this->glyphs.size());
219 CTRunGetPositions(run, CFRangeMake(0, 0), pts.get());
220 auto advs = std::make_unique<CGSize[]>(this->glyphs.size());
221 CTRunGetAdvances(run, CFRangeMake(0, 0), advs.get());
222 this->positions.reserve(this->glyphs.size());
223
224 /* Convert glyph array to our data type. At the same time, substitute
225 * the proper glyphs for our private sprite glyphs. */
226 auto gl = std::make_unique<CGGlyph[]>(this->glyphs.size());
227 CTRunGetGlyphs(run, CFRangeMake(0, 0), gl.get());
228 for (size_t i = 0; i < this->glyphs.size(); i++) {
229 if (buff[this->glyph_to_char[i]] >= SCC_SPRITE_START && buff[this->glyph_to_char[i]] <= SCC_SPRITE_END && (gl[i] == 0 || gl[i] == 3)) {
230 /* A glyph of 0 indicates not found, while apparently 3 is what char 0xFFFC maps to. */
231 this->glyphs[i] = font->fc->MapCharToGlyph(buff[this->glyph_to_char[i]]);
232 this->positions.emplace_back(pts[i].x, pts[i].x + advs[i].width - 1, (font->fc->GetHeight() - ScaleSpriteTrad(FontCache::GetDefaultFontHeight(font->fc->GetSize()))) / 2); // Align sprite font to centre
233 } else {
234 this->glyphs[i] = gl[i];
235 this->positions.emplace_back(pts[i].x, pts[i].x + advs[i].width - 1, pts[i].y);
236 }
237 }
238 this->total_advance = (int)std::ceil(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nullptr, nullptr, nullptr));
239}
240
246{
247 int leading = 0;
248 for (const auto &run : *this) {
249 leading = std::max(leading, run.GetLeading());
250 }
251
252 return leading;
253}
254
260{
261 if (this->empty()) return 0;
262
263 int total_width = 0;
264 for (const auto &run : *this) {
265 total_width += run.GetAdvance();
266 }
267
268 return total_width;
269}
270
271
277{
278 _font_cache[size].reset();
279}
280
285void MacOSRegisterExternalFont(std::string_view file_path)
286{
287 CFAutoRelease<CFStringRef> path(CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(file_path.data()), file_path.size(), kCFStringEncodingUTF8, false));
288 CFAutoRelease<CFURLRef> url(CFURLCreateWithFileSystemPath(kCFAllocatorDefault, path.get(), kCFURLPOSIXPathStyle, false));
289
290 CTFontManagerRegisterFontsForURL(url.get(), kCTFontManagerScopeProcess, nullptr);
291}
292
297void MacOSSetCurrentLocaleName(std::string_view iso_code)
298{
299 CFAutoRelease<CFStringRef> iso(CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(iso_code.data()), iso_code.size(), kCFStringEncodingUTF8, false));
300 _osx_locale.reset(CFLocaleCreate(kCFAllocatorDefault, iso.get()));
301}
302
310int MacOSStringCompare(std::string_view s1, std::string_view s2)
311{
312 CFStringCompareFlags flags = kCFCompareCaseInsensitive | kCFCompareNumerically | kCFCompareLocalized | kCFCompareWidthInsensitive | kCFCompareForcedOrdering;
313
314 CFAutoRelease<CFStringRef> cf1(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)s1.data(), s1.size(), kCFStringEncodingUTF8, false));
315 CFAutoRelease<CFStringRef> cf2(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)s2.data(), s2.size(), kCFStringEncodingUTF8, false));
316
317 /* If any CFString could not be created (e.g., due to UTF8 invalid chars), return OS unsupported functionality */
318 if (cf1 == nullptr || cf2 == nullptr) return 0;
319
320 return (int)CFStringCompareWithOptionsAndLocale(cf1.get(), cf2.get(), CFRangeMake(0, CFStringGetLength(cf1.get())), flags, _osx_locale.get()) + 2;
321}
322
331int MacOSStringContains(std::string_view str, std::string_view value, bool case_insensitive)
332{
333 CFStringCompareFlags flags = kCFCompareLocalized | kCFCompareWidthInsensitive;
334 if (case_insensitive) flags |= kCFCompareCaseInsensitive;
335
336 CFAutoRelease<CFStringRef> cf_str(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)str.data(), str.size(), kCFStringEncodingUTF8, false));
337 CFAutoRelease<CFStringRef> cf_value(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)value.data(), value.size(), kCFStringEncodingUTF8, false));
338
339 /* If any CFString could not be created (e.g., due to UTF8 invalid chars), return OS unsupported functionality */
340 if (cf_str == nullptr || cf_value == nullptr) return -1;
341
342 return CFStringFindWithOptionsAndLocale(cf_str.get(), cf_value.get(), CFRangeMake(0, CFStringGetLength(cf_str.get())), flags, _osx_locale.get(), nullptr) ? 1 : 0;
343}
344
345
346/* virtual */ void OSXStringIterator::SetString(std::string_view s)
347{
348 this->utf16_to_utf8.clear();
349 this->str_info.clear();
350 this->cur_pos = 0;
351
352 /* CoreText operates on UTF-16, thus we have to convert the input string.
353 * To be able to return proper offsets, we have to create a mapping at the same time. */
354 std::vector<UniChar> utf16_str;
355 Utf8View view(s);
356 for (auto it = view.begin(), end = view.end(); it != end; ++it) {
357 size_t idx = it.GetByteOffset();
358 char32_t c = *it;
359 if (c < 0x10000) {
360 utf16_str.push_back((UniChar)c);
361 } else {
362 /* Make a surrogate pair. */
363 utf16_str.push_back((UniChar)(0xD800 + ((c - 0x10000) >> 10)));
364 utf16_str.push_back((UniChar)(0xDC00 + ((c - 0x10000) & 0x3FF)));
365 this->utf16_to_utf8.push_back(idx);
366 }
367 this->utf16_to_utf8.push_back(idx);
368 }
369 this->utf16_to_utf8.push_back(s.size());
370
371 /* Query CoreText for word and cluster break information. */
372 this->str_info.resize(utf16_to_utf8.size());
373
374 if (!utf16_str.empty()) {
375 CFAutoRelease<CFStringRef> str(CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, &utf16_str[0], utf16_str.size(), kCFAllocatorNull));
376
377 /* Get cluster breaks. */
378 for (CFIndex i = 0; i < CFStringGetLength(str.get()); ) {
379 CFRange r = CFStringGetRangeOfComposedCharactersAtIndex(str.get(), i);
380 this->str_info[r.location].char_stop = true;
381
382 i += r.length;
383 }
384
385 /* Get word breaks. */
386 CFAutoRelease<CFStringTokenizerRef> tokenizer(CFStringTokenizerCreate(kCFAllocatorDefault, str.get(), CFRangeMake(0, CFStringGetLength(str.get())), kCFStringTokenizerUnitWordBoundary, _osx_locale.get()));
387
388 CFStringTokenizerTokenType tokenType = kCFStringTokenizerTokenNone;
389 while ((tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer.get())) != kCFStringTokenizerTokenNone) {
390 /* Skip tokens that are white-space or punctuation tokens. */
391 if ((tokenType & kCFStringTokenizerTokenHasNonLettersMask) != kCFStringTokenizerTokenHasNonLettersMask) {
392 CFRange r = CFStringTokenizerGetCurrentTokenRange(tokenizer.get());
393 this->str_info[r.location].word_stop = true;
394 }
395 }
396 }
397
398 /* End-of-string is always a valid stopping point. */
399 this->str_info.back().char_stop = true;
400 this->str_info.back().word_stop = true;
401}
402
403/* virtual */ size_t OSXStringIterator::SetCurPosition(size_t pos)
404{
405 /* Convert incoming position to an UTF-16 string index. */
406 size_t utf16_pos = 0;
407 for (size_t i = 0; i < this->utf16_to_utf8.size(); i++) {
408 if (this->utf16_to_utf8[i] == pos) {
409 utf16_pos = i;
410 break;
411 }
412 }
413
414 /* Sanitize in case we get a position inside a grapheme cluster. */
415 while (utf16_pos > 0 && !this->str_info[utf16_pos].char_stop) utf16_pos--;
416 this->cur_pos = utf16_pos;
417
418 return this->utf16_to_utf8[this->cur_pos];
419}
420
421/* virtual */ size_t OSXStringIterator::Next(IterType what)
422{
423 assert(this->cur_pos <= this->utf16_to_utf8.size());
425
426 if (this->cur_pos == this->utf16_to_utf8.size()) return END;
427
428 do {
429 this->cur_pos++;
430 } while (this->cur_pos < this->utf16_to_utf8.size() && (what == ITER_WORD ? !this->str_info[this->cur_pos].word_stop : !this->str_info[this->cur_pos].char_stop));
431
432 return this->cur_pos == this->utf16_to_utf8.size() ? END : this->utf16_to_utf8[this->cur_pos];
433}
434
435/* virtual */ size_t OSXStringIterator::Prev(IterType what)
436{
437 assert(this->cur_pos <= this->utf16_to_utf8.size());
439
440 if (this->cur_pos == 0) return END;
441
442 do {
443 this->cur_pos--;
444 } while (this->cur_pos > 0 && (what == ITER_WORD ? !this->str_info[this->cur_pos].word_stop : !this->str_info[this->cur_pos].char_stop));
445
446 return this->utf16_to_utf8[this->cur_pos];
447}
448
449/* static */ std::unique_ptr<StringIterator> OSXStringIterator::Create()
450{
451 return std::make_unique<OSXStringIterator>();
452}
UniChar CharType
Helper for GetLayouter, to get the right type.
Definition string_osx.h:44
static std::unique_ptr< ParagraphLayouter > GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &font_mapping)
Get the actual ParagraphLayout for the given buffer.
const VisualRun & GetVisualRun(size_t run) const override
Get a reference to the given run.
int GetWidth() const override
Get the width of this line.
size_t CountRuns() const override
Get the number of runs in this line.
int GetLeading() const override
Get the height of the line.
int GetInternalCharLength(char32_t c) const override
Get the number of elements the given character occupies in the underlying text buffer of the Layouter...
std::span< const int > GetGlyphToCharMap() const override
The offset for each of the glyphs to the character run that was passed to the Layouter.
const Font * GetFont() const override
Get the font.
int GetLeading() const override
Get the font leading, or distance between the baselines of consecutive lines.
size_t GetGlyphCount() const override
Get the number of glyphs.
std::span< const Position > GetPositions() const override
Get the positions for each of the glyphs.
std::span< const GlyphID > GetGlyphs() const override
Get the glyphs to draw.
Wrapper for doing layouts with CoreText.
std::unique_ptr< const Line > NextLine(int max_width) override
Construct a new line with a maximum width.
CFIndex cur_offset
Offset from the start of the current run from where to output.
void Reflow() override
Reset the position to the start of the paragraph.
int GetHeight() const
Get the height of the font.
Definition fontcache.h:60
virtual GlyphID MapCharToGlyph(char32_t key, bool fallback=true)=0
Map a character into a glyph.
FontSize GetSize() const
Get the FontSize of the font.
Definition fontcache.h:54
Container with information about a font.
Definition gfx_layout.h:97
FontCache * fc
The font we are using.
Definition gfx_layout.h:99
size_t Prev(IterType what) override
Move the cursor back by one iteration unit.
size_t Next(IterType what) override
Advance the cursor by one iteration unit.
std::vector< CharInfo > str_info
Break information for each code point.
Definition string_osx.h:24
size_t SetCurPosition(size_t pos) override
Change the current string cursor.
size_t cur_pos
Current iteration position.
Definition string_osx.h:27
std::vector< size_t > utf16_to_utf8
Mapping from UTF-16 code point position to index in the UTF-8 source string.
Definition string_osx.h:25
void SetString(std::string_view s) override
Set a new iteration string.
A single line worth of VisualRuns.
Definition gfx_layout.h:176
Visual run contains data about the bit of text with the same font.
Definition gfx_layout.h:133
Interface to glue fallback and normal layouter into one.
Definition gfx_layout.h:111
static const size_t END
Sentinel to indicate end-of-iteration.
Definition string_base.h:25
static std::unique_ptr< StringIterator > Create()
Create a new iterator instance.
Definition string.cpp:747
IterType
Type of the iterator.
Definition string_base.h:19
@ ITER_WORD
Iterate over words.
Definition string_base.h:21
@ ITER_CHARACTER
Iterate over characters (or more exactly grapheme clusters).
Definition string_base.h:20
Constant span of UTF-8 encoded data.
Definition utf8.hpp:28
Control codes that are embedded in the translation strings.
Functions to read fonts from files and cache them.
uint GetGlyphWidth(FontSize size, char32_t key)
Get the width of a glyph.
Definition fontcache.h:173
std::vector< std::pair< int, Font * > > FontMap
Mapping from index to font.
Definition gfx_layout.h:106
FontSize
Available font sizes.
Definition gfx_type.h:248
Functions related to MacOS support.
std::unique_ptr< typename std::remove_pointer< T >::type, CFDeleter< typename std::remove_pointer< T >::type > > CFAutoRelease
Specialisation of std::unique_ptr for CoreFoundation objects.
Definition macos.h:35
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.
void MacOSRegisterExternalFont(std::string_view file_path)
Register an external font file with the CoreText system.
int MacOSStringCompare(std::string_view s1, std::string_view s2)
Compares two strings using case insensitive natural sort.
void MacOSSetCurrentLocaleName(std::string_view iso_code)
Store current language locale as a CoreFoundation locale.
static CGFloat SpriteFontGetWidth(void *ref_con)
Get the width of an encoded sprite font character.
int MacOSStringContains(std::string_view str, std::string_view value, bool case_insensitive)
Search if a string is contained in another string using the current locale.
static CFAutoRelease< CFLocaleRef > _osx_locale
Cached current locale.
void MacOSResetScriptCache(FontSize size)
Delete CoreText font reference for a specific font size.
static CFAutoRelease< CTFontRef > _font_cache[FS_END]
CoreText cache for font information, cleared when OTTD changes fonts.
Functions related to localized text support on OSX.
Functions related to OTTD's strings.
Handling of UTF-8 encoded data.
Functions related to zooming.
int ScaleSpriteTrad(int value)
Scale traditional pixel dimensions to GUI zoom level, for drawing sprites.
Definition zoom_func.h:107