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