OpenTTD Source 20251213-master-g1091fa6071
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
10#include "../../stdafx.h"
11#include "string_osx.h"
12#include "../../gfx_func.h"
13#include "../../string_func.h"
14#include "../../strings_func.h"
15#include "../../core/utf8.hpp"
16#include "../../table/control_codes.h"
17#include "../../fontcache.h"
18#include "../../zoom_func.h"
19#include "macos.h"
20
21#include <CoreFoundation/CoreFoundation.h>
22#include "../../safeguards.h"
23
24
25/* CTRunDelegateCreate is supported since MacOS X 10.5, but was only included in the SDKs starting with the 10.9 SDK. */
26#ifndef HAVE_OSX_109_SDK
27extern "C" {
28 typedef const struct __CTRunDelegate * CTRunDelegateRef;
29
30 typedef void (*CTRunDelegateDeallocateCallback) (void *refCon);
31 typedef CGFloat (*CTRunDelegateGetAscentCallback) (void *refCon);
32 typedef CGFloat (*CTRunDelegateGetDescentCallback) (void *refCon);
33 typedef CGFloat (*CTRunDelegateGetWidthCallback) (void *refCon);
34 typedef struct {
35 CFIndex version;
36 CTRunDelegateDeallocateCallback dealloc;
37 CTRunDelegateGetAscentCallback getAscent;
38 CTRunDelegateGetDescentCallback getDescent;
39 CTRunDelegateGetWidthCallback getWidth;
41
42 enum : int32_t {
43 kCTRunDelegateVersion1 = 1,
44 kCTRunDelegateCurrentVersion = kCTRunDelegateVersion1
45 };
46
47 extern const CFStringRef kCTRunDelegateAttributeName AVAILABLE_MAC_OS_X_VERSION_10_5_AND_LATER;
48
49 CTRunDelegateRef CTRunDelegateCreate(const CTRunDelegateCallbacks *callbacks, void *refCon) AVAILABLE_MAC_OS_X_VERSION_10_5_AND_LATER;
50}
51#endif /* HAVE_OSX_109_SDK */
52
56static std::unordered_map<FontIndex, CFAutoRelease<CTFontRef>> _font_cache;
57
58
63private:
64 ptrdiff_t length;
65 const FontMap &font_map;
66
68
69 CFIndex cur_offset = 0;
70
71public:
74 private:
75 std::vector<GlyphID> glyphs;
76 std::vector<Position> positions;
77 std::vector<int> glyph_to_char;
78
79 int total_advance = 0;
80 Font font;
81
82 public:
83 CoreTextVisualRun(CTRunRef run, const Font &font);
84 CoreTextVisualRun(CoreTextVisualRun &&other) = default;
85
86 std::span<const GlyphID> GetGlyphs() const override { return this->glyphs; }
87 std::span<const Position> GetPositions() const override { return this->positions; }
88 std::span<const int> GetGlyphToCharMap() const override { return this->glyph_to_char; }
89
90 const Font &GetFont() const override { return this->font; }
91 int GetLeading() const override { return GetCharacterHeight(this->font.GetFontCache().GetSize()); }
92 int GetGlyphCount() const override { return (int)this->glyphs.size(); }
93 int GetAdvance() const { return this->total_advance; }
94 };
95
97 class CoreTextLine : public std::vector<CoreTextVisualRun>, public ParagraphLayouter::Line {
98 public:
99 CoreTextLine(CFAutoRelease<CTLineRef> line, const FontMap &font_mapping)
100 {
101 CFArrayRef runs = CTLineGetGlyphRuns(line.get());
102 for (CFIndex i = 0; i < CFArrayGetCount(runs); i++) {
103 CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, i);
104
105 /* Extract font information for this run. */
106 CFRange chars = CTRunGetStringRange(run);
107 const auto &map = std::ranges::upper_bound(font_mapping, chars.location, std::less{}, &std::pair<int, Font>::first);
108
109 this->emplace_back(run, map->second);
110 }
111 }
112
113 int GetLeading() const override;
114 int GetWidth() const override;
115 int CountRuns() const override { return this->size(); }
116 const VisualRun &GetVisualRun(int run) const override { return this->at(run); }
117
118 int GetInternalCharLength(char32_t c) const override
119 {
120 /* CoreText uses UTF-16 internally which means we need to account for surrogate pairs. */
121 return c >= 0x010000U ? 2 : 1;
122 }
123 };
124
125 CoreTextParagraphLayout(CFAutoRelease<CTTypesetterRef> typesetter, ptrdiff_t len, const FontMap &font_mapping) : length(len), font_map(font_mapping), typesetter(std::move(typesetter))
126 {
127 this->Reflow();
128 }
129
130 void Reflow() override
131 {
132 this->cur_offset = 0;
133 }
134
135 std::unique_ptr<const Line> NextLine(int max_width) override;
136};
137
138
140static CGFloat CustomFontGetWidth(void *ref_con)
141{
142 FontIndex fi = static_cast<FontIndex>(reinterpret_cast<uintptr_t>(ref_con) >> 24);
143 char32_t c = static_cast<char32_t>(reinterpret_cast<uintptr_t>(ref_con) & 0xFFFFFF);
144
145 return FontCache::Get(fi)->GetGlyphWidth(c);
146}
147
148static const CTRunDelegateCallbacks _sprite_font_callback = {
149 kCTRunDelegateCurrentVersion, nullptr, nullptr, nullptr,
151};
152
153/* static */ std::unique_ptr<ParagraphLayouter> CoreTextParagraphLayoutFactory::GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &font_mapping)
154{
155 if (!MacOSVersionIsAtLeast(10, 5, 0)) return nullptr;
156
157 /* Can't layout an empty string. */
158 ptrdiff_t length = buff_end - buff;
159 if (length == 0) return nullptr;
160
161 /* Make attributed string with embedded font information. */
162 CFAutoRelease<CFMutableAttributedStringRef> str(CFAttributedStringCreateMutable(kCFAllocatorDefault, 0));
163 CFAttributedStringBeginEditing(str.get());
164
165 CFAutoRelease<CFStringRef> base(CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, buff, length, kCFAllocatorNull));
166 CFAttributedStringReplaceString(str.get(), CFRangeMake(0, 0), base.get());
167
168 const UniChar replacement_char = 0xFFFC;
169 CFAutoRelease<CFStringRef> replacement_str(CFStringCreateWithCharacters(kCFAllocatorDefault, &replacement_char, 1));
170
171 /* Apply font and colour ranges to our string. This is important to make sure
172 * that we get proper glyph boundaries on style changes. */
173 int last = 0;
174 for (const auto &[position, font] : font_mapping) {
175 if (position - last == 0) continue;
176
177 FontCache &fc = font.GetFontCache();
178 CTFontRef font_handle = static_cast<CTFontRef>(fc.GetOSHandle());
179 if (font_handle == nullptr) {
180 if (!_font_cache[fc.GetIndex()]) {
181 /* Cache font information. */
182 CFAutoRelease<CFStringRef> font_name(CFStringCreateWithCString(kCFAllocatorDefault, fc.GetFontName().c_str(), kCFStringEncodingUTF8));
183 _font_cache[fc.GetIndex()].reset(CTFontCreateWithName(font_name.get(), fc.GetFontSize(), nullptr));
184 }
185 font_handle = _font_cache[fc.GetIndex()].get();
186 }
187 CFAttributedStringSetAttribute(str.get(), CFRangeMake(last, position - last), kCTFontAttributeName, font_handle);
188
189 CGColorRef colour = CGColorCreateGenericGray((uint8_t)font.colour / 255.0f, 1.0f); // We don't care about the real colours, just that they are different.
190 CFAttributedStringSetAttribute(str.get(), CFRangeMake(last, position - last), kCTForegroundColorAttributeName, colour);
191 CGColorRelease(colour);
192
193 /* Install a size callback for our custom fonts. */
194 if (fc.IsBuiltInFont()) {
195 for (ssize_t c = last; c < position; c++) {
196 CFAutoRelease<CTRunDelegateRef> del(CTRunDelegateCreate(&_sprite_font_callback, reinterpret_cast<void *>(static_cast<size_t>(buff[c] | (fc.GetIndex() << 24)))));
197 /* According to the official documentation, if a run delegate is used, the char should always be 0xFFFC. */
198 CFAttributedStringReplaceString(str.get(), CFRangeMake(c, 1), replacement_str.get());
199 CFAttributedStringSetAttribute(str.get(), CFRangeMake(c, 1), kCTRunDelegateAttributeName, del.get());
200 }
201 }
202
203 last = position;
204 }
205 CFAttributedStringEndEditing(str.get());
206
207 /* Create and return typesetter for the string. */
208 CFAutoRelease<CTTypesetterRef> typesetter(CTTypesetterCreateWithAttributedString(str.get()));
209
210 return typesetter ? std::make_unique<CoreTextParagraphLayout>(std::move(typesetter), length, font_mapping) : nullptr;
211}
212
213/* virtual */ std::unique_ptr<const ParagraphLayouter::Line> CoreTextParagraphLayout::NextLine(int max_width)
214{
215 if (this->cur_offset >= this->length) return nullptr;
216
217 /* Get line break position, trying word breaking first and breaking somewhere if that doesn't work. */
218 CFIndex len = CTTypesetterSuggestLineBreak(this->typesetter.get(), this->cur_offset, max_width);
219 if (len <= 0) len = CTTypesetterSuggestClusterBreak(this->typesetter.get(), this->cur_offset, max_width);
220
221 /* Create line. */
222 CFAutoRelease<CTLineRef> line(CTTypesetterCreateLine(this->typesetter.get(), CFRangeMake(this->cur_offset, len)));
223 this->cur_offset += len;
224
225 if (!line) return nullptr;
226 return std::make_unique<CoreTextLine>(std::move(line), this->font_map);
227}
228
229CoreTextParagraphLayout::CoreTextVisualRun::CoreTextVisualRun(CTRunRef run, const Font &font) : font(font)
230{
231 this->glyphs.resize(CTRunGetGlyphCount(run));
232
233 /* Query map of glyphs to source string index. */
234 auto map = std::make_unique<CFIndex[]>(this->glyphs.size());
235 CTRunGetStringIndices(run, CFRangeMake(0, 0), map.get());
236
237 this->glyph_to_char.resize(this->glyphs.size());
238 for (size_t i = 0; i < this->glyph_to_char.size(); i++) this->glyph_to_char[i] = (int)map[i];
239
240 auto pts = std::make_unique<CGPoint[]>(this->glyphs.size());
241 CTRunGetPositions(run, CFRangeMake(0, 0), pts.get());
242 auto advs = std::make_unique<CGSize[]>(this->glyphs.size());
243 CTRunGetAdvances(run, CFRangeMake(0, 0), advs.get());
244 this->positions.reserve(this->glyphs.size());
245
246 int y_offset = this->font.GetFontCache().GetGlyphYOffset();
247
248 /* Convert glyph array to our data type. At the same time, substitute
249 * the proper glyphs for our private sprite glyphs. */
250 auto gl = std::make_unique<CGGlyph[]>(this->glyphs.size());
251 CTRunGetGlyphs(run, CFRangeMake(0, 0), gl.get());
252 for (size_t i = 0; i < this->glyphs.size(); i++) {
253 this->glyphs[i] = gl[i];
254 this->positions.emplace_back(pts[i].x, pts[i].x + advs[i].width - 1, pts[i].y + y_offset);
255 }
256 this->total_advance = (int)std::ceil(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nullptr, nullptr, nullptr));
257}
258
264{
265 int leading = 0;
266 for (const auto &run : *this) {
267 leading = std::max(leading, run.GetLeading());
268 }
269
270 return leading;
271}
272
278{
279 if (this->empty()) return 0;
280
281 int total_width = 0;
282 for (const auto &run : *this) {
283 total_width += run.GetAdvance();
284 }
285
286 return total_width;
287}
288
289
292{
293 _font_cache[size].reset();
294}
295
297void MacOSRegisterExternalFont(std::string_view file_path)
298{
299 if (!MacOSVersionIsAtLeast(10, 6, 0)) return;
300
301 CFAutoRelease<CFStringRef> path(CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(file_path.data()), file_path.size(), kCFStringEncodingUTF8, false));
302 CFAutoRelease<CFURLRef> url(CFURLCreateWithFileSystemPath(kCFAllocatorDefault, path.get(), kCFURLPOSIXPathStyle, false));
303
304 CTFontManagerRegisterFontsForURL(url.get(), kCTFontManagerScopeProcess, nullptr);
305}
306
308void MacOSSetCurrentLocaleName(std::string_view iso_code)
309{
310 if (!MacOSVersionIsAtLeast(10, 5, 0)) return;
311
312 CFAutoRelease<CFStringRef> iso(CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(iso_code.data()), iso_code.size(), kCFStringEncodingUTF8, false));
313 _osx_locale.reset(CFLocaleCreate(kCFAllocatorDefault, iso.get()));
314}
315
323int MacOSStringCompare(std::string_view s1, std::string_view s2)
324{
325 static const bool supported = MacOSVersionIsAtLeast(10, 5, 0);
326 if (!supported) return 0;
327
328 CFStringCompareFlags flags = kCFCompareCaseInsensitive | kCFCompareNumerically | kCFCompareLocalized | kCFCompareWidthInsensitive | kCFCompareForcedOrdering;
329
330 CFAutoRelease<CFStringRef> cf1(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)s1.data(), s1.size(), kCFStringEncodingUTF8, false));
331 CFAutoRelease<CFStringRef> cf2(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)s2.data(), s2.size(), kCFStringEncodingUTF8, false));
332
333 /* If any CFString could not be created (e.g., due to UTF8 invalid chars), return OS unsupported functionality */
334 if (cf1 == nullptr || cf2 == nullptr) return 0;
335
336 return (int)CFStringCompareWithOptionsAndLocale(cf1.get(), cf2.get(), CFRangeMake(0, CFStringGetLength(cf1.get())), flags, _osx_locale.get()) + 2;
337}
338
347int MacOSStringContains(std::string_view str, std::string_view value, bool case_insensitive)
348{
349 static const bool supported = MacOSVersionIsAtLeast(10, 5, 0);
350 if (!supported) return -1;
351
352 CFStringCompareFlags flags = kCFCompareLocalized | kCFCompareWidthInsensitive;
353 if (case_insensitive) flags |= kCFCompareCaseInsensitive;
354
355 CFAutoRelease<CFStringRef> cf_str(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)str.data(), str.size(), kCFStringEncodingUTF8, false));
356 CFAutoRelease<CFStringRef> cf_value(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)value.data(), value.size(), kCFStringEncodingUTF8, false));
357
358 /* If any CFString could not be created (e.g., due to UTF8 invalid chars), return OS unsupported functionality */
359 if (cf_str == nullptr || cf_value == nullptr) return -1;
360
361 return CFStringFindWithOptionsAndLocale(cf_str.get(), cf_value.get(), CFRangeMake(0, CFStringGetLength(cf_str.get())), flags, _osx_locale.get(), nullptr) ? 1 : 0;
362}
363
364
365/* virtual */ void OSXStringIterator::SetString(std::string_view s)
366{
367 this->utf16_to_utf8.clear();
368 this->str_info.clear();
369 this->cur_pos = 0;
370
371 /* CoreText operates on UTF-16, thus we have to convert the input string.
372 * To be able to return proper offsets, we have to create a mapping at the same time. */
373 std::vector<UniChar> utf16_str;
374 Utf8View view(s);
375 for (auto it = view.begin(), end = view.end(); it != end; ++it) {
376 size_t idx = it.GetByteOffset();
377 char32_t c = *it;
378 if (c < 0x10000) {
379 utf16_str.push_back((UniChar)c);
380 } else {
381 /* Make a surrogate pair. */
382 utf16_str.push_back((UniChar)(0xD800 + ((c - 0x10000) >> 10)));
383 utf16_str.push_back((UniChar)(0xDC00 + ((c - 0x10000) & 0x3FF)));
384 this->utf16_to_utf8.push_back(idx);
385 }
386 this->utf16_to_utf8.push_back(idx);
387 }
388 this->utf16_to_utf8.push_back(s.size());
389
390 /* Query CoreText for word and cluster break information. */
391 this->str_info.resize(utf16_to_utf8.size());
392
393 if (!utf16_str.empty()) {
394 CFAutoRelease<CFStringRef> str(CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, &utf16_str[0], utf16_str.size(), kCFAllocatorNull));
395
396 /* Get cluster breaks. */
397 for (CFIndex i = 0; i < CFStringGetLength(str.get()); ) {
398 CFRange r = CFStringGetRangeOfComposedCharactersAtIndex(str.get(), i);
399 this->str_info[r.location].char_stop = true;
400
401 i += r.length;
402 }
403
404 /* Get word breaks. */
405 CFAutoRelease<CFStringTokenizerRef> tokenizer(CFStringTokenizerCreate(kCFAllocatorDefault, str.get(), CFRangeMake(0, CFStringGetLength(str.get())), kCFStringTokenizerUnitWordBoundary, _osx_locale.get()));
406
407 CFStringTokenizerTokenType tokenType = kCFStringTokenizerTokenNone;
408 while ((tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer.get())) != kCFStringTokenizerTokenNone) {
409 /* Skip tokens that are white-space or punctuation tokens. */
410 if ((tokenType & kCFStringTokenizerTokenHasNonLettersMask) != kCFStringTokenizerTokenHasNonLettersMask) {
411 CFRange r = CFStringTokenizerGetCurrentTokenRange(tokenizer.get());
412 this->str_info[r.location].word_stop = true;
413 }
414 }
415 }
416
417 /* End-of-string is always a valid stopping point. */
418 this->str_info.back().char_stop = true;
419 this->str_info.back().word_stop = true;
420}
421
422/* virtual */ size_t OSXStringIterator::SetCurPosition(size_t pos)
423{
424 /* Convert incoming position to an UTF-16 string index. */
425 size_t utf16_pos = 0;
426 for (size_t i = 0; i < this->utf16_to_utf8.size(); i++) {
427 if (this->utf16_to_utf8[i] == pos) {
428 utf16_pos = i;
429 break;
430 }
431 }
432
433 /* Sanitize in case we get a position inside a grapheme cluster. */
434 while (utf16_pos > 0 && !this->str_info[utf16_pos].char_stop) utf16_pos--;
435 this->cur_pos = utf16_pos;
436
437 return this->utf16_to_utf8[this->cur_pos];
438}
439
440/* virtual */ size_t OSXStringIterator::Next(IterType what)
441{
442 assert(this->cur_pos <= this->utf16_to_utf8.size());
444
445 if (this->cur_pos == this->utf16_to_utf8.size()) return END;
446
447 do {
448 this->cur_pos++;
449 } 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));
450
451 return this->cur_pos == this->utf16_to_utf8.size() ? END : this->utf16_to_utf8[this->cur_pos];
452}
453
454/* virtual */ size_t OSXStringIterator::Prev(IterType what)
455{
456 assert(this->cur_pos <= this->utf16_to_utf8.size());
458
459 if (this->cur_pos == 0) return END;
460
461 do {
462 this->cur_pos--;
463 } while (this->cur_pos > 0 && (what == ITER_WORD ? !this->str_info[this->cur_pos].word_stop : !this->str_info[this->cur_pos].char_stop));
464
465 return this->utf16_to_utf8[this->cur_pos];
466}
467
468/* static */ std::unique_ptr<StringIterator> OSXStringIterator::Create()
469{
470 if (!MacOSVersionIsAtLeast(10, 5, 0)) return nullptr;
471
472 return std::make_unique<OSXStringIterator>();
473}
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.
A single line worth of VisualRuns.
int GetWidth() const override
Get the width of this line.
int GetLeading() const override
Get the height of the line.
Visual run contains data about the bit of text with the same font.
Wrapper for doing layouts with CoreText.
CFIndex cur_offset
Offset from the start of the current run from where to output.
Font cache for basic fonts.
Definition fontcache.h:32
virtual std::string GetFontName()=0
Get the name of this font.
static std::span< const std::unique_ptr< FontCache > > Get()
Get span of all FontCaches.
Definition fontcache.h:177
virtual int GetFontSize() const
Get the nominal font size of the font.
Definition fontcache.h:124
virtual const void * GetOSHandle()
Get the native OS font handle, if there is one.
Definition fontcache.h:160
virtual bool IsBuiltInFont()=0
Is this a built-in sprite font?
FontSize GetSize() const
Get the FontSize of the font.
Definition fontcache.h:96
Container with information about a font.
Definition gfx_layout.h:98
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.
size_t SetCurPosition(size_t pos) override
Change the current string cursor.
void SetString(std::string_view s) override
Set a new iteration string.
A single line worth of VisualRuns.
Definition gfx_layout.h:142
Visual run contains data about the bit of text with the same font.
Definition gfx_layout.h:130
Interface to glue fallback and normal layouter into one.
Definition gfx_layout.h:112
IterType
Type of the iterator.
Definition string_base.h:17
@ ITER_WORD
Iterate over words.
Definition string_base.h:19
@ ITER_CHARACTER
Iterate over characters (or more exactly grapheme clusters).
Definition string_base.h:18
Constant span of UTF-8 encoded data.
Definition utf8.hpp:30
int GetCharacterHeight(FontSize size)
Get height of a character for a given font size.
std::vector< std::pair< int, Font > > FontMap
Mapping from index to font.
Definition gfx_layout.h:107
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:54
bool MacOSVersionIsAtLeast(long major, long minor, long bugfix)
Check if we are at least running on the specified version of Mac OS.
Definition macos.h:25
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.
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 std::unordered_map< FontIndex, CFAutoRelease< CTFontRef > > _font_cache
CoreText cache for font information, cleared when OTTD changes fonts.
static CFAutoRelease< CFLocaleRef > _osx_locale
Cached current locale.
static CGFloat CustomFontGetWidth(void *ref_con)
Get the width of an encoded sprite font character.
void MacOSResetScriptCache(FontSize size)
Delete CoreText font reference for a specific font size.
Functions related to localized text support on OSX.