basalt-appkit 0.0.0-rc5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/cli/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Turning React Native's AttributedString into something Core Text can lay out.
|
|
2
|
+
//
|
|
3
|
+
// The counterpart of basalt-gtk's PangoTextLayout, and it exists
|
|
4
|
+
// for the same reason: this is the one place that knows both halves, and it is
|
|
5
|
+
// deliberately used by both sides of the text problem.
|
|
6
|
+
//
|
|
7
|
+
// - `TextLayoutManager::measure` (CoreTextLayoutManager.mm) builds a layout to
|
|
8
|
+
// ask Core Text how big the text is, so Yoga can lay it out.
|
|
9
|
+
// - `AppKitMountingManager` builds the same layout and hands it to the view,
|
|
10
|
+
// which draws it.
|
|
11
|
+
//
|
|
12
|
+
// Measuring and painting must agree, so they must not build layouts
|
|
13
|
+
// differently. Sharing this is what guarantees that.
|
|
14
|
+
//
|
|
15
|
+
// Unlike the Pango side, nothing here needs a mutex. Core Text is documented as
|
|
16
|
+
// thread-safe, and Fabric measures on its layout thread while AppKit draws on
|
|
17
|
+
// the main one. Pango's font map is not, which is why that file serialises
|
|
18
|
+
// every call and this one does not.
|
|
19
|
+
//
|
|
20
|
+
// The paragraph object itself is in RnTextLayout.h, which has no React Native in
|
|
21
|
+
// it, so the view layer can draw one without depending on React Native.
|
|
22
|
+
|
|
23
|
+
#pragma once
|
|
24
|
+
|
|
25
|
+
#import "RnTextLayout.h"
|
|
26
|
+
|
|
27
|
+
#include <react/renderer/attributedstring/AttributedString.h>
|
|
28
|
+
#include <react/renderer/attributedstring/ParagraphAttributes.h>
|
|
29
|
+
#include <react/renderer/attributedstring/TextAttributes.h>
|
|
30
|
+
|
|
31
|
+
namespace basalt {
|
|
32
|
+
|
|
33
|
+
// Builds a layout for `attributedString`.
|
|
34
|
+
RnTextLayout *buildTextLayout(const facebook::react::AttributedString &attributedString,
|
|
35
|
+
const facebook::react::ParagraphAttributes ¶graphAttributes);
|
|
36
|
+
|
|
37
|
+
// The attributes for a whole string, for anything that holds its own text
|
|
38
|
+
// rather than a layout built here -- the equivalent of Pango's
|
|
39
|
+
// buildTextAttributes, and what a <TextInput> will need.
|
|
40
|
+
NSDictionary<NSAttributedStringKey, id> *buildTextAttributes(
|
|
41
|
+
const facebook::react::TextAttributes &textAttributes);
|
|
42
|
+
|
|
43
|
+
} // namespace basalt
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#import "CoreTextLayout.h"
|
|
2
|
+
|
|
3
|
+
#include "FontRegistry.h"
|
|
4
|
+
|
|
5
|
+
#include <react/renderer/graphics/Color.h>
|
|
6
|
+
|
|
7
|
+
#include <cmath>
|
|
8
|
+
#include <string>
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// AttributedString -> NSAttributedString
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
namespace basalt {
|
|
16
|
+
|
|
17
|
+
using facebook::react::AttributedString;
|
|
18
|
+
using facebook::react::EllipsizeMode;
|
|
19
|
+
using facebook::react::FontStyle;
|
|
20
|
+
using facebook::react::FontWeight;
|
|
21
|
+
using facebook::react::ParagraphAttributes;
|
|
22
|
+
using facebook::react::TextAlignment;
|
|
23
|
+
using facebook::react::TextAttributes;
|
|
24
|
+
using facebook::react::TextDecorationLineType;
|
|
25
|
+
|
|
26
|
+
namespace {
|
|
27
|
+
|
|
28
|
+
// React Native's default when a <Text> sets no fontSize.
|
|
29
|
+
constexpr float kDefaultFontSize = 14.0F;
|
|
30
|
+
|
|
31
|
+
bool isSet(facebook::react::Float value) {
|
|
32
|
+
return !std::isnan(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// CSS numeric weights to Core Text's -1..1 scale, using Apple's own constants
|
|
36
|
+
// for the named steps. A straight linear mapping would put "bold" somewhere
|
|
37
|
+
// between semibold and heavy and make every bold face on the system slightly
|
|
38
|
+
// wrong.
|
|
39
|
+
CGFloat toCoreTextWeight(FontWeight weight) {
|
|
40
|
+
switch (static_cast<int>(weight)) {
|
|
41
|
+
case 100: return NSFontWeightUltraLight;
|
|
42
|
+
case 200: return NSFontWeightThin;
|
|
43
|
+
case 300: return NSFontWeightLight;
|
|
44
|
+
case 400: return NSFontWeightRegular;
|
|
45
|
+
case 500: return NSFontWeightMedium;
|
|
46
|
+
case 600: return NSFontWeightSemibold;
|
|
47
|
+
case 700: return NSFontWeightBold;
|
|
48
|
+
case 800: return NSFontWeightHeavy;
|
|
49
|
+
case 900: return NSFontWeightBlack;
|
|
50
|
+
default: break;
|
|
51
|
+
}
|
|
52
|
+
return NSFontWeightRegular;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
NSTextAlignment toTextAlignment(TextAlignment alignment) {
|
|
56
|
+
switch (alignment) {
|
|
57
|
+
case TextAlignment::Center:
|
|
58
|
+
return NSTextAlignmentCenter;
|
|
59
|
+
case TextAlignment::Right:
|
|
60
|
+
#if BASALT_RN_MINOR >= 87
|
|
61
|
+
case TextAlignment::End:
|
|
62
|
+
#endif
|
|
63
|
+
return NSTextAlignmentRight;
|
|
64
|
+
case TextAlignment::Justified:
|
|
65
|
+
return NSTextAlignmentJustified;
|
|
66
|
+
case TextAlignment::Left:
|
|
67
|
+
#if BASALT_RN_MINOR >= 87
|
|
68
|
+
case TextAlignment::Start:
|
|
69
|
+
#endif
|
|
70
|
+
case TextAlignment::Natural:
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
return NSTextAlignmentNatural;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
NSColor *toColor(const facebook::react::SharedColor &color) {
|
|
77
|
+
const auto components = facebook::react::colorComponentsFromColor(color);
|
|
78
|
+
// sRGB explicitly, for the same reason RnAppKitView builds its background
|
|
79
|
+
// that way: a device colour space shifts every colour on a wide-gamut
|
|
80
|
+
// display, and text that does not match Linux is the bug this project is
|
|
81
|
+
// least able to afford.
|
|
82
|
+
return [NSColor colorWithSRGBRed:components.red
|
|
83
|
+
green:components.green
|
|
84
|
+
blue:components.blue
|
|
85
|
+
alpha:components.alpha];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
NSFont *fontFor(const TextAttributes &textAttributes) {
|
|
89
|
+
float size = isSet(textAttributes.fontSize) ? static_cast<float>(textAttributes.fontSize)
|
|
90
|
+
: kDefaultFontSize;
|
|
91
|
+
if (isSet(textAttributes.fontSizeMultiplier) && textAttributes.fontSizeMultiplier > 0) {
|
|
92
|
+
size *= static_cast<float>(textAttributes.fontSizeMultiplier);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const CGFloat weight = textAttributes.fontWeight ? toCoreTextWeight(*textAttributes.fontWeight)
|
|
96
|
+
: NSFontWeightRegular;
|
|
97
|
+
const bool italic = textAttributes.fontStyle &&
|
|
98
|
+
(*textAttributes.fontStyle == FontStyle::Italic ||
|
|
99
|
+
*textAttributes.fontStyle == FontStyle::Oblique);
|
|
100
|
+
|
|
101
|
+
NSFont *font = nil;
|
|
102
|
+
if (textAttributes.fontFamily.empty()) {
|
|
103
|
+
// The system font, which is what "no fontFamily" means on a Mac -- and
|
|
104
|
+
// asking for it by name would get the wrong thing, since San Francisco is
|
|
105
|
+
// not available under its own family name.
|
|
106
|
+
font = [NSFont systemFontOfSize:size weight:weight];
|
|
107
|
+
} else {
|
|
108
|
+
// Through the runtime registry: a font an app loaded is known to the app by
|
|
109
|
+
// a name it chose and to Core Text by the name inside the file.
|
|
110
|
+
// resolveFontFamily returns the argument unchanged for system families.
|
|
111
|
+
const std::string family = resolveFontFamily(textAttributes.fontFamily);
|
|
112
|
+
NSString *familyName = [NSString stringWithUTF8String:family.c_str()];
|
|
113
|
+
|
|
114
|
+
NSFontDescriptor *descriptor = [NSFontDescriptor fontDescriptorWithFontAttributes:@{
|
|
115
|
+
NSFontFamilyAttribute : familyName,
|
|
116
|
+
NSFontTraitsAttribute : @{NSFontWeightTrait : @(weight)},
|
|
117
|
+
}];
|
|
118
|
+
font = [NSFont fontWithDescriptor:descriptor size:size];
|
|
119
|
+
if (font == nil) {
|
|
120
|
+
// A family the system does not have. Falling back to the system font
|
|
121
|
+
// rather than to nil, because a nil font makes Core Text drop the run
|
|
122
|
+
// entirely and the text silently disappears.
|
|
123
|
+
font = [NSFont systemFontOfSize:size weight:weight];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (italic) {
|
|
128
|
+
NSFontDescriptor *italicised =
|
|
129
|
+
[font.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontDescriptorTraitItalic];
|
|
130
|
+
NSFont *slanted = [NSFont fontWithDescriptor:italicised size:size];
|
|
131
|
+
if (slanted != nil) {
|
|
132
|
+
font = slanted;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return font;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
NSParagraphStyle *paragraphStyleFor(const TextAttributes &textAttributes) {
|
|
140
|
+
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
|
|
141
|
+
|
|
142
|
+
if (textAttributes.alignment) {
|
|
143
|
+
style.alignment = toTextAlignment(*textAttributes.alignment);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// React Native's lineHeight is the total line box height, and setting both
|
|
147
|
+
// bounds to it is how that is said in AppKit. Leaving one of them out gives a
|
|
148
|
+
// minimum or a maximum, which is a different thing and only shows up on text
|
|
149
|
+
// whose natural height is on the other side of the value.
|
|
150
|
+
if (isSet(textAttributes.lineHeight)) {
|
|
151
|
+
const CGFloat lineHeight = static_cast<CGFloat>(textAttributes.lineHeight);
|
|
152
|
+
style.minimumLineHeight = lineHeight;
|
|
153
|
+
style.maximumLineHeight = lineHeight;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Wrapping, always. The line *limit* is applied in RnTextLayout, not here:
|
|
157
|
+
// a truncating line break mode would make Core Text ellipsize each line it
|
|
158
|
+
// lays out rather than the last one kept.
|
|
159
|
+
style.lineBreakMode = NSLineBreakByWordWrapping;
|
|
160
|
+
|
|
161
|
+
return style;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} // namespace
|
|
165
|
+
|
|
166
|
+
NSDictionary<NSAttributedStringKey, id> *buildTextAttributes(const TextAttributes &textAttributes) {
|
|
167
|
+
NSMutableDictionary<NSAttributedStringKey, id> *attributes = [NSMutableDictionary dictionary];
|
|
168
|
+
|
|
169
|
+
attributes[NSFontAttributeName] = fontFor(textAttributes);
|
|
170
|
+
attributes[NSParagraphStyleAttributeName] = paragraphStyleFor(textAttributes);
|
|
171
|
+
|
|
172
|
+
if (textAttributes.foregroundColor) {
|
|
173
|
+
attributes[NSForegroundColorAttributeName] = toColor(textAttributes.foregroundColor);
|
|
174
|
+
} else {
|
|
175
|
+
// React Native's default is opaque black, not the system label colour --
|
|
176
|
+
// which is white in dark mode and would make default text invisible on a
|
|
177
|
+
// light background, or vice versa. Matching React Native rather than the
|
|
178
|
+
// platform is the right call here: the same app has to look the same on
|
|
179
|
+
// Linux.
|
|
180
|
+
attributes[NSForegroundColorAttributeName] = [NSColor colorWithSRGBRed:0 green:0 blue:0 alpha:1];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (textAttributes.backgroundColor) {
|
|
184
|
+
attributes[NSBackgroundColorAttributeName] = toColor(textAttributes.backgroundColor);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (isSet(textAttributes.letterSpacing)) {
|
|
188
|
+
attributes[NSKernAttributeName] = @(static_cast<CGFloat>(textAttributes.letterSpacing));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (textAttributes.textDecorationLineType) {
|
|
192
|
+
switch (*textAttributes.textDecorationLineType) {
|
|
193
|
+
case TextDecorationLineType::Underline:
|
|
194
|
+
attributes[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
|
|
195
|
+
break;
|
|
196
|
+
case TextDecorationLineType::Strikethrough:
|
|
197
|
+
attributes[NSStrikethroughStyleAttributeName] = @(NSUnderlineStyleSingle);
|
|
198
|
+
break;
|
|
199
|
+
case TextDecorationLineType::UnderlineStrikethrough:
|
|
200
|
+
attributes[NSUnderlineStyleAttributeName] = @(NSUnderlineStyleSingle);
|
|
201
|
+
attributes[NSStrikethroughStyleAttributeName] = @(NSUnderlineStyleSingle);
|
|
202
|
+
break;
|
|
203
|
+
case TextDecorationLineType::None:
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return attributes;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
RnTextLayout *buildTextLayout(const AttributedString &attributedString,
|
|
212
|
+
const ParagraphAttributes ¶graphAttributes) {
|
|
213
|
+
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init];
|
|
214
|
+
|
|
215
|
+
for (const auto &fragment : attributedString.getFragments()) {
|
|
216
|
+
if (fragment.string.empty()) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
NSString *text = [NSString stringWithUTF8String:fragment.string.c_str()];
|
|
220
|
+
if (text == nil) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
[string appendAttributedString:[[NSAttributedString alloc]
|
|
224
|
+
initWithString:text
|
|
225
|
+
attributes:buildTextAttributes(fragment.textAttributes)]];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
CTLineTruncationType truncation = kCTLineTruncationEnd;
|
|
229
|
+
bool truncates = true;
|
|
230
|
+
switch (paragraphAttributes.ellipsizeMode) {
|
|
231
|
+
case EllipsizeMode::Head:
|
|
232
|
+
truncation = kCTLineTruncationStart;
|
|
233
|
+
break;
|
|
234
|
+
case EllipsizeMode::Middle:
|
|
235
|
+
truncation = kCTLineTruncationMiddle;
|
|
236
|
+
break;
|
|
237
|
+
case EllipsizeMode::Tail:
|
|
238
|
+
truncation = kCTLineTruncationEnd;
|
|
239
|
+
break;
|
|
240
|
+
case EllipsizeMode::Clip:
|
|
241
|
+
// Cut, with no ellipsis. The line limit still applies.
|
|
242
|
+
truncates = false;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return [RnTextLayout layoutWithAttributedString:string
|
|
247
|
+
maximumNumberOfLines:paragraphAttributes.maximumNumberOfLines
|
|
248
|
+
truncationType:truncation
|
|
249
|
+
truncates:truncates ? YES : NO];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
} // namespace basalt
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The macOS implementation of React Native's text measurement seam.
|
|
2
|
+
//
|
|
3
|
+
// `TextLayoutManager` is declared in React Native's cxx platform variant, and
|
|
4
|
+
// its only implementation there is a stub that ignores every attribute and
|
|
5
|
+
// returns `layoutConstraints.minimumSize`. cmake/ReactNativeCore.cmake drops
|
|
6
|
+
// that stub from the build, so a platform either provides this or fails to
|
|
7
|
+
// link -- which is the second of the three seams phase 19 found, and the reason
|
|
8
|
+
// macOS could not register a Paragraph descriptor until now.
|
|
9
|
+
//
|
|
10
|
+
// The header is unchanged and shared; only the .cpp differs, which is what
|
|
11
|
+
// React Native's platform/ split is for. Its Linux counterpart is
|
|
12
|
+
// basalt-gtk's PangoTextLayoutManager.cpp, and the two are
|
|
13
|
+
// deliberately the same file with a different engine inside.
|
|
14
|
+
//
|
|
15
|
+
// Yoga calls this during layout, on whichever thread is committing, so it must
|
|
16
|
+
// be thread-safe and it must be fast.
|
|
17
|
+
|
|
18
|
+
#import "CoreTextLayout.h"
|
|
19
|
+
|
|
20
|
+
#include "FontRegistry.h"
|
|
21
|
+
|
|
22
|
+
#include <algorithm>
|
|
23
|
+
#include <cmath>
|
|
24
|
+
#include <mutex>
|
|
25
|
+
#include <unordered_map>
|
|
26
|
+
|
|
27
|
+
#include <react/renderer/textlayoutmanager/TextLayoutManager.h>
|
|
28
|
+
|
|
29
|
+
namespace facebook::react {
|
|
30
|
+
|
|
31
|
+
namespace {
|
|
32
|
+
|
|
33
|
+
std::mutex &measurementCacheMutex() {
|
|
34
|
+
static std::mutex mutex;
|
|
35
|
+
return mutex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Emptied whenever a font is registered; see the note at its first use.
|
|
39
|
+
std::unordered_map<TextMeasureCacheKey, TextMeasurement> &measurementCache() {
|
|
40
|
+
static std::unordered_map<TextMeasureCacheKey, TextMeasurement> cache;
|
|
41
|
+
static unsigned long generation = 0;
|
|
42
|
+
const unsigned long current = basalt::fontGeneration();
|
|
43
|
+
if (current != generation) {
|
|
44
|
+
cache.clear();
|
|
45
|
+
generation = current;
|
|
46
|
+
}
|
|
47
|
+
return cache;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
} // namespace
|
|
51
|
+
|
|
52
|
+
TextLayoutManager::TextLayoutManager(const std::shared_ptr<const ContextContainer> &contextContainer)
|
|
53
|
+
: contextContainer_(contextContainer), textMeasureCache_(kSimpleThreadSafeCacheSizeCap) {}
|
|
54
|
+
|
|
55
|
+
TextMeasurement TextLayoutManager::measure(const AttributedStringBox &attributedStringBox,
|
|
56
|
+
const ParagraphAttributes ¶graphAttributes,
|
|
57
|
+
const TextLayoutContext &layoutContext,
|
|
58
|
+
const LayoutConstraints &layoutConstraints) const {
|
|
59
|
+
const auto &attributedString = attributedStringBox.getValue();
|
|
60
|
+
|
|
61
|
+
// pointScaleFactor joined the cache key in React Native 0.87. On 0.81 the key
|
|
62
|
+
// has three fields and naming a fourth is a compile error. Its absence only
|
|
63
|
+
// makes the cache coarser there, and since nothing below uses the factor,
|
|
64
|
+
// coarser is harmless.
|
|
65
|
+
const TextMeasureCacheKey key{
|
|
66
|
+
.attributedString = attributedString,
|
|
67
|
+
.paragraphAttributes = paragraphAttributes,
|
|
68
|
+
.layoutConstraints = layoutConstraints,
|
|
69
|
+
#if BASALT_RN_MINOR >= 87
|
|
70
|
+
.pointScaleFactor = layoutContext.pointScaleFactor,
|
|
71
|
+
#endif
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
#if BASALT_RN_MINOR < 87
|
|
75
|
+
(void)layoutContext;
|
|
76
|
+
#endif
|
|
77
|
+
|
|
78
|
+
// Not React Native's `textMeasureCache_`, which has no way to be emptied.
|
|
79
|
+
//
|
|
80
|
+
// A font registered since a measurement invalidates it: the same string with
|
|
81
|
+
// the same attributes measures differently once its family exists. That is
|
|
82
|
+
// not expressible in the cache key, which React Native defines, so the cache
|
|
83
|
+
// has to be droppable instead -- and without that, a font loaded after first
|
|
84
|
+
// render, which is how `useFonts` and every other loader works, appears to do
|
|
85
|
+
// nothing at all.
|
|
86
|
+
{
|
|
87
|
+
const std::lock_guard<std::mutex> lock(measurementCacheMutex());
|
|
88
|
+
auto &cache = measurementCache();
|
|
89
|
+
const auto hit = cache.find(key);
|
|
90
|
+
if (hit != cache.end()) {
|
|
91
|
+
return hit->second;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
TextMeasurement measured;
|
|
96
|
+
@autoreleasepool {
|
|
97
|
+
// An infinite maximum width means "do not wrap".
|
|
98
|
+
const CGFloat maxWidth = std::isinf(layoutConstraints.maximumSize.width)
|
|
99
|
+
? -1.0
|
|
100
|
+
: static_cast<CGFloat>(layoutConstraints.maximumSize.width);
|
|
101
|
+
|
|
102
|
+
RnTextLayout *layout = basalt::buildTextLayout(attributedString, paragraphAttributes);
|
|
103
|
+
const CGSize size = [layout sizeForWidth:maxWidth];
|
|
104
|
+
|
|
105
|
+
// Core Text can report a line slightly wider than the width it was given,
|
|
106
|
+
// for an unbreakable run. Yoga treats the returned size as final, so
|
|
107
|
+
// clamping here keeps the paragraph inside the box its parent allotted.
|
|
108
|
+
const auto natural = Size{
|
|
109
|
+
.width = std::min(static_cast<Float>(size.width), layoutConstraints.maximumSize.width),
|
|
110
|
+
.height = static_cast<Float>(size.height),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Inline views (`<Text><View/></Text>`) reach here as attachment fragments.
|
|
114
|
+
// Reporting a zero frame for each keeps the count right, which is what
|
|
115
|
+
// ParagraphShadowNode iterates over, but they are not positioned yet --
|
|
116
|
+
// the same gap the GTK side has, for the same reason.
|
|
117
|
+
TextMeasurement::Attachments attachments;
|
|
118
|
+
for (const auto &fragment : attributedString.getFragments()) {
|
|
119
|
+
if (fragment.isAttachment()) {
|
|
120
|
+
attachments.push_back(TextMeasurement::Attachment{
|
|
121
|
+
.frame = {.origin = {.x = 0, .y = 0}, .size = {.width = 0, .height = 0}},
|
|
122
|
+
.isClipped = false,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
measured = TextMeasurement{.size = layoutConstraints.clamp(natural), .attachments = attachments};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
{
|
|
131
|
+
const std::lock_guard<std::mutex> lock(measurementCacheMutex());
|
|
132
|
+
measurementCache().emplace(key, measured);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return measured;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
} // namespace facebook::react
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// The font seam on macOS, backed by Core Text.
|
|
2
|
+
//
|
|
3
|
+
// The counterpart of gtk/FontRegistryFontconfig.cpp, and the first seam phase
|
|
4
|
+
// 17 identified: `expo-font` hands over a file and a name of the app's choosing
|
|
5
|
+
// -- "Inter", say -- and expects `fontFamily: 'Inter'` to work afterwards. A
|
|
6
|
+
// font file carries its own family name inside it, usually something else
|
|
7
|
+
// entirely, and Core Text indexes by that. So loading the file is only half the
|
|
8
|
+
// job; the other half is remembering that this app calls it "Inter".
|
|
9
|
+
//
|
|
10
|
+
// Registration is process-scoped: the font becomes available to this process
|
|
11
|
+
// and to nothing else on the machine, which is what an app loading a bundled
|
|
12
|
+
// font wants and the only scope that needs no user consent.
|
|
13
|
+
//
|
|
14
|
+
// Nothing on this platform renders text yet -- there is no Core Text
|
|
15
|
+
// TextLayoutManager, which is the seam phase 19 found. So `resolveFontFamily`
|
|
16
|
+
// currently has no reader. It is implemented anyway, because the alternative is
|
|
17
|
+
// a registry that silently records nothing and a font milestone that starts by
|
|
18
|
+
// debugging this file.
|
|
19
|
+
|
|
20
|
+
#include "FontRegistry.h"
|
|
21
|
+
|
|
22
|
+
#import <CoreText/CoreText.h>
|
|
23
|
+
#import <Foundation/Foundation.h>
|
|
24
|
+
|
|
25
|
+
#include <mutex>
|
|
26
|
+
#include <unordered_map>
|
|
27
|
+
#include <vector>
|
|
28
|
+
|
|
29
|
+
namespace basalt {
|
|
30
|
+
|
|
31
|
+
namespace {
|
|
32
|
+
|
|
33
|
+
struct Registry {
|
|
34
|
+
std::mutex mutex;
|
|
35
|
+
unsigned long generation{0};
|
|
36
|
+
// What the app calls it -> what the file calls itself.
|
|
37
|
+
std::unordered_map<std::string, std::string> families;
|
|
38
|
+
std::vector<std::string> order;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
Registry ®istry() {
|
|
42
|
+
static Registry instance;
|
|
43
|
+
return instance;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The family name recorded inside the file. Core Text will register a file and
|
|
47
|
+
// then index it under a name the app has never heard of, so this is read back
|
|
48
|
+
// rather than assumed -- the same reason the fontconfig side reads it.
|
|
49
|
+
//
|
|
50
|
+
// A file can hold several faces; the first one's family is used, which matches
|
|
51
|
+
// what a single-font asset means and is what fontconfig's equivalent does.
|
|
52
|
+
std::string familyInFile(NSURL *url) {
|
|
53
|
+
NSArray *descriptors =
|
|
54
|
+
(__bridge_transfer NSArray *)CTFontManagerCreateFontDescriptorsFromURL((__bridge CFURLRef)url);
|
|
55
|
+
if (descriptors.count == 0) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
CTFontDescriptorRef descriptor = (__bridge CTFontDescriptorRef)descriptors[0];
|
|
60
|
+
NSString *family = (__bridge_transfer NSString *)CTFontDescriptorCopyAttribute(
|
|
61
|
+
descriptor, kCTFontFamilyNameAttribute);
|
|
62
|
+
if (family == nil) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
return family.UTF8String;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
} // namespace
|
|
69
|
+
|
|
70
|
+
bool registerFont(const std::string &name, const std::string &path) {
|
|
71
|
+
Registry &state = registry();
|
|
72
|
+
const std::lock_guard<std::mutex> lock(state.mutex);
|
|
73
|
+
|
|
74
|
+
if (state.families.find(name) != state.families.end()) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@autoreleasepool {
|
|
79
|
+
NSURL *url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:path.c_str()]];
|
|
80
|
+
if (url == nil) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
CFErrorRef error = nullptr;
|
|
85
|
+
if (!CTFontManagerRegisterFontsForURL((__bridge CFURLRef)url,
|
|
86
|
+
kCTFontManagerScopeProcess,
|
|
87
|
+
&error)) {
|
|
88
|
+
// Already registered is not a failure: two apps' worth of the same file,
|
|
89
|
+
// or a second call for a different app-facing name, both land here and
|
|
90
|
+
// both leave the font usable.
|
|
91
|
+
const bool alreadyRegistered =
|
|
92
|
+
error != nullptr && CFErrorGetCode(error) == kCTFontManagerErrorAlreadyRegistered;
|
|
93
|
+
if (error != nullptr) {
|
|
94
|
+
CFRelease(error);
|
|
95
|
+
}
|
|
96
|
+
if (!alreadyRegistered) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const std::string family = familyInFile(url);
|
|
102
|
+
if (family.empty()) {
|
|
103
|
+
// Registered but unusable: without the real family there is no way to ask
|
|
104
|
+
// for it, and pretending otherwise would fail later and further away.
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
state.families.emplace(name, family);
|
|
109
|
+
state.order.push_back(name);
|
|
110
|
+
state.generation++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
unsigned long fontGeneration() {
|
|
117
|
+
Registry &state = registry();
|
|
118
|
+
const std::lock_guard<std::mutex> lock(state.mutex);
|
|
119
|
+
return state.generation;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
std::string resolveFontFamily(const std::string &name) {
|
|
123
|
+
Registry &state = registry();
|
|
124
|
+
const std::lock_guard<std::mutex> lock(state.mutex);
|
|
125
|
+
const auto it = state.families.find(name);
|
|
126
|
+
return it == state.families.end() ? name : it->second;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
bool isFontRegistered(const std::string &name) {
|
|
130
|
+
Registry &state = registry();
|
|
131
|
+
const std::lock_guard<std::mutex> lock(state.mutex);
|
|
132
|
+
return state.families.find(name) != state.families.end();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
std::string registeredFontNamesJoined(char separator) {
|
|
136
|
+
Registry &state = registry();
|
|
137
|
+
const std::lock_guard<std::mutex> lock(state.mutex);
|
|
138
|
+
std::string joined;
|
|
139
|
+
for (const auto &name : state.order) {
|
|
140
|
+
if (!joined.empty()) {
|
|
141
|
+
joined.push_back(separator);
|
|
142
|
+
}
|
|
143
|
+
joined += name;
|
|
144
|
+
}
|
|
145
|
+
return joined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
} // namespace basalt
|