OpenTTD Source 20260108-master-g8ba1860eaa
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 "../../string_func.h"
13#include "../../strings_func.h"
14#include "../../core/utf8.hpp"
15#include "../../table/control_codes.h"
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
34private:
36 ptrdiff_t length;
37 const FontMap &font_map;
38
40
41 CFIndex cur_offset = 0;
42
43public:
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 int GetGlyphCount() const override { return (int)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:
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 int CountRuns() const override { return this->size(); }
88 const VisualRun &GetVisualRun(int 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
112static CGFloat SpriteFontGetWidth(void *ref_con)
113{
114 FontSize fs = (FontSize)((size_t)ref_con >> 24);
115 char32_t c = (char32_t)((size_t)ref_con & 0xFFFFFF);
116
117 return GetGlyphWidth(fs, c);
118}
119
120static const CTRunDelegateCallbacks _sprite_font_callback = {
121 kCTRunDelegateCurrentVersion, nullptr, nullptr, nullptr,
123};
124
125/* static */ std::unique_ptr<ParagraphLayouter> CoreTextParagraphLayoutFactory::GetParagraphLayout(CharType *buff, CharType *buff_end, FontMap &font_mapping)
126{
127 if (!MacOSVersionIsAtLeast(10, 5, 0)) return nullptr;
128
129 /* Can't layout an empty string. */
130 ptrdiff_t length = buff_end - buff;
131 if (length == 0) return nullptr;
132
133 /* Can't layout our in-built sprite fonts. */
134 for (const auto &[position, font] : font_mapping) {
135 if (font->fc->IsBuiltInFont()) return nullptr;
136 }
137
138 /* Make attributed string with embedded font information. */
139 CFAutoRelease<CFMutableAttributedStringRef> str(CFAttributedStringCreateMutable(kCFAllocatorDefault, 0));
140 CFAttributedStringBeginEditing(str.get());
141
142 CFAutoRelease<CFStringRef> base(CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, buff, length, kCFAllocatorNull));
143 CFAttributedStringReplaceString(str.get(), CFRangeMake(0, 0), base.get());
144
145 const UniChar replacement_char = 0xFFFC;
146 CFAutoRelease<CFStringRef> replacement_str(CFStringCreateWithCharacters(kCFAllocatorDefault, &replacement_char, 1));
147
148 /* Apply font and colour ranges to our string. This is important to make sure
149 * that we get proper glyph boundaries on style changes. */
150 int last = 0;
151 for (const auto &[position, font] : font_mapping) {
152 if (position - last == 0) continue;
153
154 CTFontRef font_handle = static_cast<CTFontRef>(font->fc->GetOSHandle());
155 if (font_handle == nullptr) {
156 if (!_font_cache[font->fc->GetSize()]) {
157 /* Cache font information. */
158 CFAutoRelease<CFStringRef> font_name(CFStringCreateWithCString(kCFAllocatorDefault, font->fc->GetFontName().c_str(), kCFStringEncodingUTF8));
159 _font_cache[font->fc->GetSize()].reset(CTFontCreateWithName(font_name.get(), font->fc->GetFontSize(), nullptr));
160 }
161 font_handle = _font_cache[font->fc->GetSize()].get();
162 }
163 CFAttributedStringSetAttribute(str.get(), CFRangeMake(last, position - last), kCTFontAttributeName, font_handle);
164
165 CGColorRef colour = CGColorCreateGenericGray((uint8_t)font->colour / 255.0f, 1.0f); // We don't care about the real colours, just that they are different.
166 CFAttributedStringSetAttribute(str.get(), CFRangeMake(last, position - last), kCTForegroundColorAttributeName, colour);
167 CGColorRelease(colour);
168
169 /* Install a size callback for our special private-use sprite glyphs in case the font does not provide them. */
170 for (ssize_t c = last; c < position; c++) {
171 if (buff[c] >= SCC_SPRITE_START && buff[c] <= SCC_SPRITE_END && font->fc->MapCharToGlyph(buff[c], false) == 0) {
172 CFAutoRelease<CTRunDelegateRef> del(CTRunDelegateCreate(&_sprite_font_callback, (void *)(size_t)(buff[c] | (font->fc->GetSize() << 24))));
173 /* According to the official documentation, if a run delegate is used, the char should always be 0xFFFC. */
174 CFAttributedStringReplaceString(str.get(), CFRangeMake(c, 1), replacement_str.get());
175 CFAttributedStringSetAttribute(str.get(), CFRangeMake(c, 1), kCTRunDelegateAttributeName, del.get());
176 }
177 }
178
179 last = position;
180 }
181 CFAttributedStringEndEditing(str.get());
182
183 /* Create and return typesetter for the string. */
184 CFAutoRelease<CTTypesetterRef> typesetter(CTTypesetterCreateWithAttributedString(str.get()));
185
186 return typesetter ? std::make_unique<CoreTextParagraphLayout>(std::move(typesetter), buff, length, font_mapping) : nullptr;
187}
188
189/* virtual */ std::unique_ptr<const ParagraphLayouter::Line> CoreTextParagraphLayout::NextLine(int max_width)
190{
191 if (this->cur_offset >= this->length) return nullptr;
192
193 /* Get line break position, trying word breaking first and breaking somewhere if that doesn't work. */
194 CFIndex len = CTTypesetterSuggestLineBreak(this->typesetter.get(), this->cur_offset, max_width);
195 if (len <= 0) len = CTTypesetterSuggestClusterBreak(this->typesetter.get(), this->cur_offset, max_width);
196
197 /* Create line. */
198 CFAutoRelease<CTLineRef> line(CTTypesetterCreateLine(this->typesetter.get(), CFRangeMake(this->cur_offset, len)));
199 this->cur_offset += len;
200
201 if (!line) return nullptr;
202 return std::make_unique<CoreTextLine>(std::move(line), this->font_map, this->text_buffer);
203}
204
205CoreTextParagraphLayout::CoreTextVisualRun::CoreTextVisualRun(CTRunRef run, Font *font, const CoreTextParagraphLayoutFactory::CharType *buff) : font(font)
206{
207 this->glyphs.resize(CTRunGetGlyphCount(run));
208
209 /* Query map of glyphs to source string index. */
210 auto map = std::make_unique<CFIndex[]>(this->glyphs.size());
211 CTRunGetStringIndices(run, CFRangeMake(0, 0), map.get());
212
213 this->glyph_to_char.resize(this->glyphs.size());
214 for (size_t i = 0; i < this->glyph_to_char.size(); i++) this->glyph_to_char[i] = (int)map[i];
215
216 auto pts = std::make_unique<CGPoint[]>(this->glyphs.size());
217 CTRunGetPositions(run, CFRangeMake(0, 0), pts.get());
218 auto advs = std::make_unique<CGSize[]>(this->glyphs.size());
219 CTRunGetAdvances(run, CFRangeMake(0, 0), advs.get());
220 this->positions.reserve(this->glyphs.size());
221
222 /* Convert glyph array to our data type. At the same time, substitute
223 * the proper glyphs for our private sprite glyphs. */
224 auto gl = std::make_unique<CGGlyph[]>(this->glyphs.size());
225 CTRunGetGlyphs(run, CFRangeMake(0, 0), gl.get());
226 for (size_t i = 0; i < this->glyphs.size(); i++) {
227 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)) {
228 /* A glyph of 0 indicates not found, while apparently 3 is what char 0xFFFC maps to. */
229 this->glyphs[i] = font->fc->MapCharToGlyph(buff[this->glyph_to_char[i]]);
230 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
231 } else {
232 this->glyphs[i] = gl[i];
233 this->positions.emplace_back(pts[i].x, pts[i].x + advs[i].width - 1, pts[i].y);
234 }
235 }
236 this->total_advance = (int)std::ceil(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), nullptr, nullptr, nullptr));
237}
238
244{
245 int leading = 0;
246 for (const auto &run : *this) {
247 leading = std::max(leading, run.GetLeading());
248 }
249
250 return leading;
251}
252
258{
259 if (this->empty()) return 0;
260
261 int total_width = 0;
262 for (const auto &run : *this) {
263 total_width += run.GetAdvance();
264 }
265
266 return total_width;
267}
268
269
272{
273 _font_cache[size].reset();
274}
275
277void MacOSRegisterExternalFont(std::string_view file_path)
278{
279 if (!MacOSVersionIsAtLeast(10, 6, 0)) return;
280
281 CFAutoRelease<CFStringRef> path(CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(file_path.data()), file_path.size(), kCFStringEncodingUTF8, false));
282 CFAutoRelease<CFURLRef> url(CFURLCreateWithFileSystemPath(kCFAllocatorDefault, path.get(), kCFURLPOSIXPathStyle, false));
283
284 CTFontManagerRegisterFontsForURL(url.get(), kCTFontManagerScopeProcess, nullptr);
285}
286
288void MacOSSetCurrentLocaleName(std::string_view iso_code)
289{
290 if (!MacOSVersionIsAtLeast(10, 5, 0)) return;
291
292 CFAutoRelease<CFStringRef> iso(CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast<const UInt8 *>(iso_code.data()), iso_code.size(), kCFStringEncodingUTF8, false));
293 _osx_locale.reset(CFLocaleCreate(kCFAllocatorDefault, iso.get()));
294}
295
303int MacOSStringCompare(std::string_view s1, std::string_view s2)
304{
305 static const bool supported = MacOSVersionIsAtLeast(10, 5, 0);
306 if (!supported) return 0;
307
308 CFStringCompareFlags flags = kCFCompareCaseInsensitive | kCFCompareNumerically | kCFCompareLocalized | kCFCompareWidthInsensitive | kCFCompareForcedOrdering;
309
310 CFAutoRelease<CFStringRef> cf1(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)s1.data(), s1.size(), kCFStringEncodingUTF8, false));
311 CFAutoRelease<CFStringRef> cf2(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)s2.data(), s2.size(), kCFStringEncodingUTF8, false));
312
313 /* If any CFString could not be created (e.g., due to UTF8 invalid chars), return OS unsupported functionality */
314 if (cf1 == nullptr || cf2 == nullptr) return 0;
315
316 return (int)CFStringCompareWithOptionsAndLocale(cf1.get(), cf2.get(), CFRangeMake(0, CFStringGetLength(cf1.get())), flags, _osx_locale.get()) + 2;
317}
318
327int MacOSStringContains(std::string_view str, std::string_view value, bool case_insensitive)
328{
329 static const bool supported = MacOSVersionIsAtLeast(10, 5, 0);
330 if (!supported) return -1;
331
332 CFStringCompareFlags flags = kCFCompareLocalized | kCFCompareWidthInsensitive;
333 if (case_insensitive) flags |= kCFCompareCaseInsensitive;
334
335 CFAutoRelease<CFStringRef> cf_str(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)str.data(), str.size(), kCFStringEncodingUTF8, false));
336 CFAutoRelease<CFStringRef> cf_value(CFStringCreateWithBytes(kCFAllocatorDefault, (const UInt8 *)value.data(), value.size(), kCFStringEncodingUTF8, false));
337
338 /* If any CFString could not be created (e.g., due to UTF8 invalid chars), return OS unsupported functionality */
339 if (cf_str == nullptr || cf_value == nullptr) return -1;
340
341 return CFStringFindWithOptionsAndLocale(cf_str.get(), cf_value.get(), CFRangeMake(0, CFStringGetLength(cf_str.get())), flags, _osx_locale.get(), nullptr) ? 1 : 0;
342}
343
344
345/* virtual */ void OSXStringIterator::SetString(std::string_view s)
346{
347 this->utf16_to_utf8.clear();
348 this->str_info.clear();
349 this->cur_pos = 0;
350
351 /* CoreText operates on UTF-16, thus we have to convert the input string.
352 * To be able to return proper offsets, we have to create a mapping at the same time. */
353 std::vector<UniChar> utf16_str;
354 Utf8View view(s);
355 for (auto it = view.begin(), end = view.end(); it != end; ++it) {
356 size_t idx = it.GetByteOffset();
357 char32_t c = *it;
358 if (c < 0x10000) {
359 utf16_str.push_back((UniChar)c);
360 } else {
361 /* Make a surrogate pair. */
362 utf16_str.push_back((UniChar)(0xD800 + ((c - 0x10000) >> 10)));
363 utf16_str.push_back((UniChar)(0xDC00 + ((c - 0x10000) & 0x3FF)));
364 this->utf16_to_utf8.push_back(idx);
365 }
366 this->utf16_to_utf8.push_back(idx);
367 }
368 this->utf16_to_utf8.push_back(s.size());
369
370 /* Query CoreText for word and cluster break information. */
371 this->str_info.resize(utf16_to_utf8.size());
372
373 if (!utf16_str.empty()) {
374 CFAutoRelease<CFStringRef> str(CFStringCreateWithCharactersNoCopy(kCFAllocatorDefault, &utf16_str[0], utf16_str.size(), kCFAllocatorNull));
375
376 /* Get cluster breaks. */
377 for (CFIndex i = 0; i < CFStringGetLength(str.get()); ) {
378 CFRange r = CFStringGetRangeOfComposedCharactersAtIndex(str.get(), i);
379 this->str_info[r.location].char_stop = true;
380
381 i += r.length;
382 }
383
384 /* Get word breaks. */
385 CFAutoRelease<CFStringTokenizerRef> tokenizer(CFStringTokenizerCreate(kCFAllocatorDefault, str.get(), CFRangeMake(0, CFStringGetLength(str.get())), kCFStringTokenizerUnitWordBoundary, _osx_locale.get()));
386
387 CFStringTokenizerTokenType tokenType = kCFStringTokenizerTokenNone;
388 while ((tokenType = CFStringTokenizerAdvanceToNextToken(tokenizer.get())) != kCFStringTokenizerTokenNone) {
389 /* Skip tokens that are white-space or punctuation tokens. */
390 if ((tokenType & kCFStringTokenizerTokenHasNonLettersMask) != kCFStringTokenizerTokenHasNonLettersMask) {
391 CFRange r = CFStringTokenizerGetCurrentTokenRange(tokenizer.get());
392 this->str_info[r.location].word_stop = true;
393 }
394 }
395 }
396
397 /* End-of-string is always a valid stopping point. */
398 this->str_info.back().char_stop = true;
399 this->str_info.back().word_stop = true;
400}
401
402/* virtual */ size_t OSXStringIterator::SetCurPosition(size_t pos)
403{
404 /* Convert incoming position to an UTF-16 string index. */
405 size_t utf16_pos = 0;
406 for (size_t i = 0; i < this->utf16_to_utf8.size(); i++) {
407 if (this->utf16_to_utf8[i] == pos) {
408 utf16_pos = i;
409 break;
410 }
411 }
412
413 /* Sanitize in case we get a position inside a grapheme cluster. */
414 while (utf16_pos > 0 && !this->str_info[utf16_pos].char_stop) utf16_pos--;
415 this->cur_pos = utf16_pos;
416
417 return this->utf16_to_utf8[this->cur_pos];
418}
419
420/* virtual */ size_t OSXStringIterator::Next(IterType what)
421{
422 assert(this->cur_pos <= this->utf16_to_utf8.size());
424
425 if (this->cur_pos == this->utf16_to_utf8.size()) return END;
426
427 do {
428 this->cur_pos++;
429 } 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));
430
431 return this->cur_pos == this->utf16_to_utf8.size() ? END : this->utf16_to_utf8[this->cur_pos];
432}
433
434/* virtual */ size_t OSXStringIterator::Prev(IterType what)
435{
436 assert(this->cur_pos <= this->utf16_to_utf8.size());
438
439 if (this->cur_pos == 0) return END;
440
441 do {
442 this->cur_pos--;
443 } while (this->cur_pos > 0 && (what == ITER_WORD ? !this->str_info[this->cur_pos].word_stop : !this->str_info[this->cur_pos].char_stop));
444
445 return this->utf16_to_utf8[this->cur_pos];
446}
447
448/* static */ std::unique_ptr<StringIterator> OSXStringIterator::Create()
449{
450 if (!MacOSVersionIsAtLeast(10, 5, 0)) return nullptr;
451
452 return std::make_unique<OSXStringIterator>();
453}
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.
int GetHeight() const
Get the height of the font.
Definition fontcache.h:59
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:53
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.
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:141
Visual run contains data about the bit of text with the same font.
Definition gfx_layout.h:129
Interface to glue fallback and normal layouter into one.
Definition gfx_layout.h:111
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
uint GetGlyphWidth(FontSize size, char32_t key)
Get the width of a glyph.
Definition fontcache.h:160
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: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.
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.
int ScaleSpriteTrad(int value)
Scale traditional pixel dimensions to GUI zoom level, for drawing sprites.
Definition zoom_func.h:107