basalt-core 0.0.0-rc5 → 0.2.0
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/dist/cli/basalt.d.ts.map +1 -1
- package/dist/cli/basalt.js +18 -1
- package/dist/cli/basalt.js.map +1 -1
- package/dist/cli/bundle-cli.js +0 -0
- package/dist/cli/desktop.d.ts.map +1 -1
- package/dist/cli/desktop.js +53 -4
- package/dist/cli/desktop.js.map +1 -1
- package/dist/cli/packageApp.d.ts.map +1 -1
- package/dist/cli/packageApp.js +7 -1
- package/dist/cli/packageApp.js.map +1 -1
- package/dist/metro-config.d.ts.map +1 -1
- package/dist/metro-config.js +53 -5
- package/dist/metro-config.js.map +1 -1
- package/dist/src/KeyHandler.d.ts +1 -1
- package/dist/src/KeyHandler.d.ts.map +1 -1
- package/dist/src/KeyHandler.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/overrides/Alert.d.ts +1 -1
- package/dist/src/overrides/Alert.js +1 -1
- package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
- package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
- package/dist/src/overrides/BaseViewConfig.js +21 -3
- package/dist/src/overrides/BaseViewConfig.js.map +1 -1
- package/dist/src/overrides/Button.d.ts +81 -0
- package/dist/src/overrides/Button.d.ts.map +1 -0
- package/dist/src/overrides/Button.js +109 -0
- package/dist/src/overrides/Button.js.map +1 -0
- package/dist/src/overrides/ScrollView.d.ts +25 -2
- package/dist/src/overrides/ScrollView.d.ts.map +1 -1
- package/dist/src/overrides/ScrollView.js +64 -7
- package/dist/src/overrides/ScrollView.js.map +1 -1
- package/dist/src/overrides/Settings.d.ts +81 -0
- package/dist/src/overrides/Settings.d.ts.map +1 -0
- package/dist/src/overrides/Settings.js +120 -0
- package/dist/src/overrides/Settings.js.map +1 -0
- package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
- package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
- package/dist/src/overrides/processDecelerationRate.js +31 -0
- package/dist/src/overrides/processDecelerationRate.js.map +1 -0
- package/dist/src/useContextMenu.d.ts +57 -6
- package/dist/src/useContextMenu.d.ts.map +1 -1
- package/dist/src/useContextMenu.js +34 -1
- package/dist/src/useContextMenu.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/native/CMakeLists.txt +85 -1
- package/native/cmake/Skia.cmake +29 -6
- package/native/core/ActionSheet.cpp +75 -0
- package/native/core/ActionSheet.h +90 -0
- package/native/core/AppStateHost.cpp +76 -0
- package/native/core/AppStateHost.h +68 -0
- package/native/core/BackgroundLayers.h +282 -0
- package/native/core/BlendModes.h +67 -0
- package/native/core/CoreModules.cpp +380 -11
- package/native/core/CoreModules.h +140 -4
- package/native/core/CrashHandler.cpp +246 -0
- package/native/core/CrashHandler.h +65 -0
- package/native/core/CursorNames.h +110 -0
- package/native/core/DevMenu.cpp +6 -5
- package/native/core/DeviceMetrics.cpp +87 -0
- package/native/core/DeviceMetrics.h +80 -0
- package/native/core/Filters.h +336 -0
- package/native/core/FontFitting.h +183 -0
- package/native/core/FontScaling.h +153 -0
- package/native/core/FontVariants.h +148 -0
- package/native/core/Gradients.h +523 -0
- package/native/core/ImageAnimation.h +109 -0
- package/native/core/ImageBytes.cpp +28 -1
- package/native/core/ImageBytes.h +17 -1
- package/native/core/ImageProgress.h +85 -0
- package/native/core/LabelRegistry.h +189 -0
- package/native/core/LegacyShadow.h +90 -0
- package/native/core/LiveRegions.h +114 -0
- package/native/core/MenuModule.cpp +35 -4
- package/native/core/MountingWalk.h +125 -2
- package/native/core/PlatformServices.h +187 -3
- package/native/core/SafeAreaComponent.cpp +115 -0
- package/native/core/SafeAreaComponent.h +105 -0
- package/native/core/ScreensComponent.cpp +201 -0
- package/native/core/ScreensComponent.h +216 -0
- package/native/core/ScrollBounds.cpp +8 -0
- package/native/core/ScrollBounds.h +14 -0
- package/native/core/ScrollIndicator.h +67 -0
- package/native/core/ScrollMomentum.cpp +24 -0
- package/native/core/ScrollMomentum.h +14 -0
- package/native/core/ScrollSnap.cpp +121 -12
- package/native/core/ScrollSnap.h +45 -0
- package/native/core/ScrollVisiblePosition.cpp +68 -0
- package/native/core/ScrollVisiblePosition.h +101 -0
- package/native/core/SettingsStore.cpp +199 -0
- package/native/core/SettingsStore.h +127 -0
- package/native/core/ShadowShape.h +38 -0
- package/native/core/SkiaViewComponent.cpp +11 -0
- package/native/core/SkiaViewComponent.h +50 -0
- package/native/core/SystemLocale.cpp +103 -0
- package/native/core/SystemLocale.h +86 -0
- package/native/core/TestDialog.cpp +45 -6
- package/native/core/TestDialog.h +15 -0
- package/native/core/TextAlignments.h +240 -0
- package/native/core/TextChecking.h +133 -0
- package/native/core/TextColors.h +83 -0
- package/native/core/TextDecorations.h +101 -0
- package/native/core/TextDirection.cpp +242 -0
- package/native/core/TextDirection.h +88 -0
- package/native/core/TextHighlight.h +30 -0
- package/native/core/TextSelection.cpp +97 -0
- package/native/core/TextSelection.h +161 -0
- package/native/core/TextShadows.h +96 -0
- package/native/core/TextVerticalAlign.h +33 -0
- package/native/core/UIManagerAccess.cpp +36 -0
- package/native/core/UIManagerAccess.h +28 -0
- package/native/core/WindowBoundsCache.cpp +58 -1
- package/native/core/WindowControl.h +24 -0
- package/native/core/WritingDirections.h +52 -0
- package/native/core/portability_probe.cpp +7 -0
- package/native/tests/RenamedFontFile.h +161 -0
- package/native/tests/TestHarness.cpp +18 -0
- package/native/tests/test_action_sheet.cpp +157 -0
- package/native/tests/test_app_state.cpp +60 -0
- package/native/tests/test_background_layers.cpp +281 -0
- package/native/tests/test_blend_modes.cpp +79 -0
- package/native/tests/test_blobs.cpp +1 -1
- package/native/tests/test_device_metrics.cpp +126 -0
- package/native/tests/test_filters.cpp +271 -0
- package/native/tests/test_font_fitting.cpp +172 -0
- package/native/tests/test_font_scaling.cpp +127 -0
- package/native/tests/test_font_variants.cpp +84 -0
- package/native/tests/test_gradients.cpp +390 -0
- package/native/tests/test_image_animation.cpp +105 -0
- package/native/tests/test_image_progress.cpp +94 -0
- package/native/tests/test_label_registry.cpp +262 -0
- package/native/tests/test_legacy_shadow.cpp +165 -0
- package/native/tests/test_live_regions.cpp +108 -0
- package/native/tests/test_menu_entries.cpp +192 -0
- package/native/tests/test_momentum.cpp +45 -0
- package/native/tests/test_scroll_bounds.cpp +42 -0
- package/native/tests/test_scroll_indicator.cpp +61 -0
- package/native/tests/test_scroll_snap.cpp +129 -0
- package/native/tests/test_scroll_visible_position.cpp +121 -0
- package/native/tests/test_settings_store.cpp +257 -0
- package/native/tests/test_shadow_shape.cpp +47 -0
- package/native/tests/test_system_locale.cpp +36 -0
- package/native/tests/test_text_alignment.cpp +299 -0
- package/native/tests/test_text_checking.cpp +31 -0
- package/native/tests/test_text_colors.cpp +92 -0
- package/native/tests/test_text_decorations.cpp +100 -0
- package/native/tests/test_text_selection.cpp +168 -0
- package/native/tests/test_text_shadows.cpp +132 -0
- package/package.json +4 -2
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#include "TextDirection.h"
|
|
2
|
+
|
|
3
|
+
#include "TextAlignments.h"
|
|
4
|
+
#include "WritingDirections.h"
|
|
5
|
+
|
|
6
|
+
#include <cstdint>
|
|
7
|
+
#include <string>
|
|
8
|
+
|
|
9
|
+
namespace basalt {
|
|
10
|
+
|
|
11
|
+
namespace {
|
|
12
|
+
|
|
13
|
+
// One code point from UTF-8, advancing `index`. Returns 0 for a byte that
|
|
14
|
+
// cannot start a sequence, which the caller treats as neutral -- a layout
|
|
15
|
+
// decision rather than a validation.
|
|
16
|
+
std::uint32_t nextCodePoint(std::string_view text, std::size_t &index) {
|
|
17
|
+
const auto byte = static_cast<unsigned char>(text[index]);
|
|
18
|
+
std::size_t length = 1;
|
|
19
|
+
std::uint32_t value = byte;
|
|
20
|
+
|
|
21
|
+
if (byte >= 0xF0) {
|
|
22
|
+
length = 4;
|
|
23
|
+
value = byte & 0x07U;
|
|
24
|
+
} else if (byte >= 0xE0) {
|
|
25
|
+
length = 3;
|
|
26
|
+
value = byte & 0x0FU;
|
|
27
|
+
} else if (byte >= 0xC0) {
|
|
28
|
+
length = 2;
|
|
29
|
+
value = byte & 0x1FU;
|
|
30
|
+
} else if (byte >= 0x80) {
|
|
31
|
+
// A continuation byte with no lead. Skip it.
|
|
32
|
+
index += 1;
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (index + length > text.size()) {
|
|
37
|
+
index = text.size();
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
for (std::size_t offset = 1; offset < length; ++offset) {
|
|
41
|
+
const auto continuation = static_cast<unsigned char>(text[index + offset]);
|
|
42
|
+
if ((continuation & 0xC0U) != 0x80U) {
|
|
43
|
+
index += offset;
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
value = (value << 6) | (continuation & 0x3FU);
|
|
47
|
+
}
|
|
48
|
+
index += length;
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The characters inside the Arabic block that are *numbers* rather than
|
|
53
|
+
// letters: Arabic-Indic digits are class AN and the extended ones are class EN,
|
|
54
|
+
// and neither is strong. Reading the block as a whole is the obvious mistake
|
|
55
|
+
// and it makes a string of Arabic-Indic digits followed by Latin text a
|
|
56
|
+
// right-to-left paragraph, which Unicode says it is
|
|
57
|
+
// not. Asked by both halves below, which is why it is its own function: the
|
|
58
|
+
// first version excluded them from the strong test and forgot the neutral one,
|
|
59
|
+
// so a digit stopped the search as though it were a Latin letter.
|
|
60
|
+
bool isArabicNumber(std::uint32_t code) {
|
|
61
|
+
return (code >= 0x0660 && code <= 0x0669) || code == 0x066B || code == 0x066C ||
|
|
62
|
+
(code >= 0x06F0 && code <= 0x06F9);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The blocks whose letters are class R or AL. Written as ranges because that is
|
|
66
|
+
// how Unicode allocates them, and ordered so a reader can check them against
|
|
67
|
+
// the standard's block list.
|
|
68
|
+
bool isStrongRightToLeft(std::uint32_t code) {
|
|
69
|
+
// Hebrew, and the two marks that are class R in their own right.
|
|
70
|
+
if (code == 0x200F /* RLM */ || code == 0x061C /* ALM */) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (code >= 0x0590 && code <= 0x05FF) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// Arabic, minus its numbers; see isArabicNumber.
|
|
77
|
+
if (code >= 0x0600 && code <= 0x06FF) {
|
|
78
|
+
return !isArabicNumber(code);
|
|
79
|
+
}
|
|
80
|
+
// Syriac, Arabic Supplement, Thaana, NKo, Samaritan, Mandaic, Syriac
|
|
81
|
+
// Supplement, Arabic Extended-B and -A, in one range: U+0700..U+08FF is
|
|
82
|
+
// right-to-left throughout.
|
|
83
|
+
if (code >= 0x0700 && code <= 0x08FF) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
// Hebrew and Arabic presentation forms.
|
|
87
|
+
if (code >= 0xFB1D && code <= 0xFDFF) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (code >= 0xFE70 && code <= 0xFEFF) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
// The historical right-to-left scripts: Cypriot through Arabic Extended-C.
|
|
94
|
+
if (code >= 0x10800 && code <= 0x10FFF) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (code >= 0x1E800 && code <= 0x1EFFF) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Everything that is not a letter of some script: skipped, because P2 asks for
|
|
104
|
+
// the first *strong* character.
|
|
105
|
+
//
|
|
106
|
+
// Spelled as what to skip rather than as what counts, because what counts is
|
|
107
|
+
// every letter of every left-to-right script and no list of those is short.
|
|
108
|
+
// What is skipped is ASCII punctuation and digits, the Latin-1 punctuation
|
|
109
|
+
// block, combining marks, general punctuation and the block of currency and
|
|
110
|
+
// symbol characters after it -- which is what sits in front of a word in real
|
|
111
|
+
// text.
|
|
112
|
+
bool isNeutral(std::uint32_t code) {
|
|
113
|
+
if (code == 0) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
if (isArabicNumber(code)) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (code < 0x0041) {
|
|
120
|
+
// Controls, space, digits and the punctuation before 'A'.
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (code >= 0x005B && code <= 0x0060) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (code >= 0x007B && code <= 0x00BF) {
|
|
127
|
+
// ASCII punctuation past 'z', the C1 controls, and Latin-1's punctuation
|
|
128
|
+
// and symbols, which end before the first accented capital.
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (code == 0x00D7 || code == 0x00F7) {
|
|
132
|
+
// Multiplication and division signs, which sit among the Latin letters.
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (code >= 0x0300 && code <= 0x036F) {
|
|
136
|
+
// Combining marks. A letter carrying one is strong; the mark is not.
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (code >= 0x2000 && code <= 0x2BFF) {
|
|
140
|
+
// General punctuation through the symbol blocks. The two right-to-left
|
|
141
|
+
// marks in here are answered before this is asked.
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (code >= 0x1F000 && code <= 0x1FAFF) {
|
|
145
|
+
// Emoji and the symbol planes.
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
} // namespace
|
|
152
|
+
|
|
153
|
+
bool textStartsRightToLeft(std::string_view utf8) {
|
|
154
|
+
std::size_t index = 0;
|
|
155
|
+
while (index < utf8.size()) {
|
|
156
|
+
const std::uint32_t code = nextCodePoint(utf8, index);
|
|
157
|
+
if (isStrongRightToLeft(code)) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (!isNeutral(code)) {
|
|
161
|
+
// The first strong left-to-right character, which ends the search: P2
|
|
162
|
+
// takes the first strong character of either kind.
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
bool paragraphIsRightToLeft(std::optional<facebook::react::WritingDirection> direction,
|
|
170
|
+
std::string_view utf8) {
|
|
171
|
+
if (direction.has_value()) {
|
|
172
|
+
switch (*direction) {
|
|
173
|
+
case facebook::react::WritingDirection::LeftToRight:
|
|
174
|
+
return false;
|
|
175
|
+
case facebook::react::WritingDirection::RightToLeft:
|
|
176
|
+
return true;
|
|
177
|
+
case facebook::react::WritingDirection::Natural:
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return textStartsRightToLeft(utf8);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
bool layoutIsRightToLeft(std::optional<facebook::react::LayoutDirection> layoutDirection) {
|
|
185
|
+
return layoutDirection.value_or(facebook::react::LayoutDirection::LeftToRight)
|
|
186
|
+
== facebook::react::LayoutDirection::RightToLeft;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
TextDirections textDirections(std::optional<facebook::react::WritingDirection> direction,
|
|
190
|
+
std::string_view utf8,
|
|
191
|
+
bool layoutRightToLeft) {
|
|
192
|
+
// What `start` and `end` mean: the direction the app *asked* for when it
|
|
193
|
+
// asked, and the layout's otherwise.
|
|
194
|
+
//
|
|
195
|
+
// Upstream's `RCTResolveTextAlignment` reads only the layout direction, so a
|
|
196
|
+
// `<Text writingDirection="rtl" textAlign="end">` is flush right on iOS. It
|
|
197
|
+
// is flush left here, which is the measured behaviour this file's table was
|
|
198
|
+
// written for: `end` is the edge a line finishes at, and a paragraph that
|
|
199
|
+
// said which way it runs has said which edge that is. Nothing is lost -- a
|
|
200
|
+
// paragraph that says nothing still follows the layout, which is upstream's
|
|
201
|
+
// whole rule -- and backlog/text.md records the divergence.
|
|
202
|
+
bool relative = layoutRightToLeft;
|
|
203
|
+
if (direction.has_value() && *direction != facebook::react::WritingDirection::Natural) {
|
|
204
|
+
relative = *direction == facebook::react::WritingDirection::RightToLeft;
|
|
205
|
+
}
|
|
206
|
+
return TextDirections{
|
|
207
|
+
.paragraph = paragraphIsRightToLeft(direction, utf8),
|
|
208
|
+
.relative = relative,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
TextDirections paragraphDirections(const facebook::react::AttributedString &attributedString) {
|
|
213
|
+
// The first fragment, the way every other paragraph-level question here is
|
|
214
|
+
// answered: React Native resolves these onto every fragment from the <Text>
|
|
215
|
+
// that owns them, and an empty paragraph has nothing to mirror anyway.
|
|
216
|
+
const auto &fragments = attributedString.getFragments();
|
|
217
|
+
const bool layoutRightToLeft =
|
|
218
|
+
!fragments.empty() && layoutIsRightToLeft(fragments.front().textAttributes.layoutDirection);
|
|
219
|
+
return textDirections(writingDirection(attributedString),
|
|
220
|
+
paragraphText(attributedString),
|
|
221
|
+
layoutRightToLeft);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const char *paragraphTextAlignmentName(
|
|
225
|
+
const facebook::react::AttributedString &attributedString) {
|
|
226
|
+
const auto &fragments = attributedString.getFragments();
|
|
227
|
+
const std::optional<facebook::react::TextAlignment> alignment = fragments.empty()
|
|
228
|
+
? std::optional<facebook::react::TextAlignment>{}
|
|
229
|
+
: fragments.front().textAttributes.alignment;
|
|
230
|
+
return physicalTextAlignmentName(
|
|
231
|
+
physicalTextAlignment(alignment, paragraphDirections(attributedString)));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
std::string paragraphText(const facebook::react::AttributedString &attributedString) {
|
|
235
|
+
std::string text;
|
|
236
|
+
for (const auto &fragment : attributedString.getFragments()) {
|
|
237
|
+
text += fragment.string;
|
|
238
|
+
}
|
|
239
|
+
return text;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
} // namespace basalt
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Which way a paragraph runs, when the app did not say.
|
|
2
|
+
//
|
|
3
|
+
// `writingDirection` is three values -- `ltr`, `rtl` and `natural` -- and
|
|
4
|
+
// `natural`, which is also the default, means "ask the text". React Native says
|
|
5
|
+
// which answer it means: `WritingDirection::Natural` is commented upstream as
|
|
6
|
+
// "determines direction using the Unicode Bidi Algorithm rules P2 and P3". P2
|
|
7
|
+
// is the direction of the first strong character, where strong means a letter of
|
|
8
|
+
// a left-to-right script (L) or of a right-to-left one (R or AL) -- digits,
|
|
9
|
+
// punctuation, spaces and symbols are not strong and are skipped -- and P3 is
|
|
10
|
+
// that a paragraph with no strong character at all runs left to right.
|
|
11
|
+
//
|
|
12
|
+
// ## Why this is here rather than asked of each engine
|
|
13
|
+
//
|
|
14
|
+
// Only one of the three engines answers it. Pango resolves the paragraph
|
|
15
|
+
// direction itself when `auto_dir` is on, and `pango_find_base_dir` is P2
|
|
16
|
+
// exactly. Core Text resolves it inside a frame, which the AppKit layer does
|
|
17
|
+
// not use -- it draws its own lines so that `numberOfLines` can be honoured --
|
|
18
|
+
// and DirectWrite does not resolve it at all: `SetReadingDirection` is told, and
|
|
19
|
+
// a paragraph of Hebrew with nothing set is laid out left to right.
|
|
20
|
+
//
|
|
21
|
+
// So two of three need this, and the alternative to one shared answer is one
|
|
22
|
+
// real answer and two approximations that differ -- which is the argument
|
|
23
|
+
// AppPaths.h makes about the resource path, for the same reason: a paragraph
|
|
24
|
+
// that aligns differently on two desktops is worse than one that aligns by a
|
|
25
|
+
// rule written down here.
|
|
26
|
+
//
|
|
27
|
+
// `test_text.cpp` in the GTK suite holds this against `pango_find_base_dir`
|
|
28
|
+
// over a list of scripts, so "the rule below is P2" is measured rather than
|
|
29
|
+
// claimed, and a disagreement is a failing test rather than a paragraph nobody
|
|
30
|
+
// looks at twice.
|
|
31
|
+
//
|
|
32
|
+
// ## What it does not do
|
|
33
|
+
//
|
|
34
|
+
// Isolates. P2 skips the characters between an isolate initiator (U+2066..68)
|
|
35
|
+
// and its matching PDI, so an isolated Hebrew word followed by Latin text is a left-to-right paragraph
|
|
36
|
+
// by the standard and a right-to-left one by this. React Native has no way to
|
|
37
|
+
// emit an isolate, an app would have to put one in a string literal, and
|
|
38
|
+
// handling them means tracking a stack -- so this is the line, and it is drawn
|
|
39
|
+
// here rather than discovered later.
|
|
40
|
+
|
|
41
|
+
#pragma once
|
|
42
|
+
|
|
43
|
+
#include "TextAlignments.h"
|
|
44
|
+
|
|
45
|
+
#include <react/renderer/attributedstring/AttributedString.h>
|
|
46
|
+
|
|
47
|
+
#include <optional>
|
|
48
|
+
#include <string_view>
|
|
49
|
+
|
|
50
|
+
namespace basalt {
|
|
51
|
+
|
|
52
|
+
// Unicode P2 over UTF-8: the first strong character's direction, or false --
|
|
53
|
+
// left to right -- when there is none. Invalid UTF-8 is skipped rather than
|
|
54
|
+
// refused; this is a layout decision, not a validator.
|
|
55
|
+
bool textStartsRightToLeft(std::string_view utf8);
|
|
56
|
+
|
|
57
|
+
// The direction a paragraph is laid out in: what the app asked for, or what the
|
|
58
|
+
// text says when it asked for `natural` or for nothing.
|
|
59
|
+
//
|
|
60
|
+
// This is the question every relative alignment needs answered, and the reason
|
|
61
|
+
// `textAlign: 'end'` was wrong on all three hosts: `end` is the edge a line
|
|
62
|
+
// finishes at, which is the left one here.
|
|
63
|
+
bool paragraphIsRightToLeft(std::optional<facebook::react::WritingDirection> direction,
|
|
64
|
+
std::string_view utf8);
|
|
65
|
+
|
|
66
|
+
// What the app's layout does, from the field React Native fills for exactly
|
|
67
|
+
// this: `TextAttributes::layoutDirection`, set in
|
|
68
|
+
// `ParagraphShadowNode::getContent` from `YGNodeLayoutGetDirection`. Absent
|
|
69
|
+
// means left to right, which is what upstream's own `.value_or` does -- a
|
|
70
|
+
// paragraph that reached a host without one is not a paragraph to guess about.
|
|
71
|
+
bool layoutIsRightToLeft(std::optional<facebook::react::LayoutDirection> layoutDirection);
|
|
72
|
+
|
|
73
|
+
// Both directions at once, which is what every caller wants: see
|
|
74
|
+
// `TextDirections` in core/TextAlignments.h for why there are two.
|
|
75
|
+
TextDirections textDirections(std::optional<facebook::react::WritingDirection> direction,
|
|
76
|
+
std::string_view utf8,
|
|
77
|
+
bool layoutRightToLeft);
|
|
78
|
+
|
|
79
|
+
// The same, for a whole paragraph: the direction prop and the text come out of
|
|
80
|
+
// the attributed string, and the layout direction off its fragments.
|
|
81
|
+
TextDirections paragraphDirections(const facebook::react::AttributedString &attributedString);
|
|
82
|
+
|
|
83
|
+
// The paragraph's text, concatenated, which is what the rule above reads. The
|
|
84
|
+
// fragments are the paragraph: React Native splits a `<Text>` into one per
|
|
85
|
+
// distinct set of attributes, and P2 asks about the whole of it.
|
|
86
|
+
std::string paragraphText(const facebook::react::AttributedString &attributedString);
|
|
87
|
+
|
|
88
|
+
} // namespace basalt
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// What a selection looks like.
|
|
2
|
+
//
|
|
3
|
+
// A colour, in one place, for the same reason `core/FocusRing.h` holds the
|
|
4
|
+
// focus ring's: a highlight that is the GTK theme's blue on one desktop,
|
|
5
|
+
// `NSColor.selectedTextBackgroundColor` on another and `COLOR_HIGHLIGHT` on a
|
|
6
|
+
// third is a difference an app did not ask for and cannot control. React Native
|
|
7
|
+
// has no prop for it either -- `selectionColor` belongs to `<TextInput>`, not
|
|
8
|
+
// to a paragraph -- so there is nothing an app could use to put the three back
|
|
9
|
+
// in step afterwards.
|
|
10
|
+
//
|
|
11
|
+
// The focus ring's blue, at the alpha a wash wants. The highlight is drawn
|
|
12
|
+
// *under* the glyphs and nothing recolours them, so it has to leave text of any
|
|
13
|
+
// colour legible through it; a solid accent with white text is the other
|
|
14
|
+
// convention and needs a second decision about the text, which React Native
|
|
15
|
+
// gave the paragraph and this platform does not get to overrule.
|
|
16
|
+
//
|
|
17
|
+
// Separate from core/TextSelection.h, which is the state machine, because each
|
|
18
|
+
// host's *widget* layer paints this and has no React Native in it: the same
|
|
19
|
+
// split core/TextVerticalAlign.h is on the other side of.
|
|
20
|
+
|
|
21
|
+
#pragma once
|
|
22
|
+
|
|
23
|
+
namespace basalt {
|
|
24
|
+
|
|
25
|
+
constexpr float kTextSelectionRed = 0.259F;
|
|
26
|
+
constexpr float kTextSelectionGreen = 0.522F;
|
|
27
|
+
constexpr float kTextSelectionBlue = 0.957F;
|
|
28
|
+
constexpr float kTextSelectionAlpha = 0.35F;
|
|
29
|
+
|
|
30
|
+
} // namespace basalt
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#include "TextSelection.h"
|
|
2
|
+
|
|
3
|
+
#include "PlatformServices.h"
|
|
4
|
+
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <utility>
|
|
7
|
+
#include <cmath>
|
|
8
|
+
|
|
9
|
+
namespace basalt {
|
|
10
|
+
|
|
11
|
+
void TextSelection::press(facebook::react::Tag tag, int index, facebook::react::Point at) {
|
|
12
|
+
// A press always ends the previous selection, whether or not this one becomes
|
|
13
|
+
// a selection of its own: clicking somewhere is how a person dismisses a
|
|
14
|
+
// highlight, and that has to happen on the press rather than on the release
|
|
15
|
+
// or the old highlight survives the click that was meant to end it.
|
|
16
|
+
tag_ = tag;
|
|
17
|
+
anchor_ = index;
|
|
18
|
+
focus_ = index;
|
|
19
|
+
origin_ = at;
|
|
20
|
+
pressed_ = tag != 0;
|
|
21
|
+
claimed_ = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
bool TextSelection::moveTo(int index, facebook::react::Point at) {
|
|
25
|
+
if (!pressed_) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!claimed_) {
|
|
30
|
+
const double dx = static_cast<double>(at.x) - static_cast<double>(origin_.x);
|
|
31
|
+
const double dy = static_cast<double>(at.y) - static_cast<double>(origin_.y);
|
|
32
|
+
if (std::sqrt(dx * dx + dy * dy) < kTextSelectionThreshold) {
|
|
33
|
+
// Still a tap as far as anybody can tell.
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
claimed_ = true;
|
|
37
|
+
focus_ = index;
|
|
38
|
+
// True exactly once per gesture, which is what the caller uses to cancel
|
|
39
|
+
// the touch sequence it had been reporting.
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
focus_ = index;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
void TextSelection::release() {
|
|
48
|
+
pressed_ = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
void TextSelection::clear() {
|
|
52
|
+
tag_ = 0;
|
|
53
|
+
anchor_ = 0;
|
|
54
|
+
focus_ = 0;
|
|
55
|
+
pressed_ = false;
|
|
56
|
+
claimed_ = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
TextSelectionRange TextSelection::range() const {
|
|
60
|
+
if (!claimed_) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
const int start = std::min(anchor_, focus_);
|
|
64
|
+
const int end = std::max(anchor_, focus_);
|
|
65
|
+
return TextSelectionRange{start, end - start};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Copying it ---------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
namespace {
|
|
71
|
+
|
|
72
|
+
std::function<std::string()> &provider() {
|
|
73
|
+
static std::function<std::string()> value;
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
} // namespace
|
|
78
|
+
|
|
79
|
+
void setSelectedTextProvider(std::function<std::string()> value) {
|
|
80
|
+
provider() = std::move(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
std::string selectedText() {
|
|
84
|
+
const std::function<std::string()> &ask = provider();
|
|
85
|
+
return ask ? ask() : std::string{};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
bool copySelectedText() {
|
|
89
|
+
const std::string text = selectedText();
|
|
90
|
+
if (text.empty()) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
setClipboardText(text);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
} // namespace basalt
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Selecting text with a pointer, which is a desktop expectation and was a
|
|
2
|
+
// `<Text selectable>` that did nothing.
|
|
3
|
+
//
|
|
4
|
+
// `BaseParagraphProps::isSelectable` is the field, and both `selectable` and
|
|
5
|
+
// `userSelect` arrive in it -- React Native's own `Text.js` maps the style onto
|
|
6
|
+
// the prop. What a host does with it is three questions, and only the third is
|
|
7
|
+
// shared: where a point lands in the text, what rectangles a range covers, and
|
|
8
|
+
// *what a press and a drag mean*. The first two are each engine's own
|
|
9
|
+
// (`pango_layout_xy_to_index`, `CTLineGetStringIndexForPosition`,
|
|
10
|
+
// `IDWriteTextLayout::HitTestPoint`); the third is this file, because it is a
|
|
11
|
+
// state machine with edges, and three hosts guessing at the edges separately is
|
|
12
|
+
// how they would come to disagree about what a click does.
|
|
13
|
+
//
|
|
14
|
+
// ## The hard part is arbitration, not the selection
|
|
15
|
+
//
|
|
16
|
+
// A press inside selectable text cannot immediately become a selection, because
|
|
17
|
+
// a press is also how a `<Pressable>` wrapping that text is pressed, and how a
|
|
18
|
+
// `<ScrollView>` containing it is dragged. Claiming on the press would break
|
|
19
|
+
// both: a tap on a selectable label inside a button would select a word and
|
|
20
|
+
// never fire `onPress`.
|
|
21
|
+
//
|
|
22
|
+
// So a press only *remembers* where it landed. The gesture becomes a selection
|
|
23
|
+
// when the pointer has moved far enough to say the person is sweeping rather
|
|
24
|
+
// than tapping, and at that moment the host cancels the touch sequence it had
|
|
25
|
+
// been reporting -- which is what the responder system does on a phone when a
|
|
26
|
+
// scroll claims a touch, and reads the same way to React: a touchstart, some
|
|
27
|
+
// moves, a cancel.
|
|
28
|
+
//
|
|
29
|
+
// A press that never moves is a tap, and a tap clears whatever was selected,
|
|
30
|
+
// which is what every desktop does.
|
|
31
|
+
//
|
|
32
|
+
// ## Indices are the host's own unit
|
|
33
|
+
//
|
|
34
|
+
// Pango counts UTF-8 bytes, Core Text and DirectWrite count UTF-16 code units,
|
|
35
|
+
// and nothing here needs them to agree: a paragraph's selection never leaves
|
|
36
|
+
// the host. There is no `onSelectionChange` for a `<Text>` -- the only event
|
|
37
|
+
// `ParagraphEventEmitter` has is `onTextLayout` -- so the indices are between a
|
|
38
|
+
// hit test and a highlight, both of which are the same engine's. What crosses
|
|
39
|
+
// to the clipboard is the substring, not an offset.
|
|
40
|
+
//
|
|
41
|
+
// So this file does no arithmetic on an index beyond comparing and ordering
|
|
42
|
+
// them, and says so here rather than inviting somebody to add a conversion.
|
|
43
|
+
|
|
44
|
+
// What a selection *looks* like is core/TextHighlight.h, which is a colour and
|
|
45
|
+
// no React Native: each host's widget layer paints it, and that layer cannot
|
|
46
|
+
// include this file. Not included from here, so the dependency stays where it
|
|
47
|
+
// is used.
|
|
48
|
+
|
|
49
|
+
#pragma once
|
|
50
|
+
|
|
51
|
+
#include <react/renderer/graphics/Point.h>
|
|
52
|
+
#include <react/renderer/core/ReactPrimitives.h>
|
|
53
|
+
|
|
54
|
+
#include <functional>
|
|
55
|
+
#include <string>
|
|
56
|
+
|
|
57
|
+
namespace basalt {
|
|
58
|
+
|
|
59
|
+
// How far a pointer has to move before a press becomes a selection rather than
|
|
60
|
+
// a tap, in points.
|
|
61
|
+
//
|
|
62
|
+
// Two, which is the smallest number that is not a shaky hand: a mouse delivers
|
|
63
|
+
// sub-pixel motion on a trackpad, and a threshold of zero would turn every
|
|
64
|
+
// click on selectable text into a selection and cancel every tap. React
|
|
65
|
+
// Native's own `Pressability` allows a touch to wander much further before it
|
|
66
|
+
// stops being a press, which is the right number for a finger and the wrong one
|
|
67
|
+
// for a cursor.
|
|
68
|
+
inline constexpr double kTextSelectionThreshold = 2.0;
|
|
69
|
+
|
|
70
|
+
// An ordered range, which is what a highlight and a copy both want. `length` is
|
|
71
|
+
// zero when nothing is selected.
|
|
72
|
+
struct TextSelectionRange {
|
|
73
|
+
int start{0};
|
|
74
|
+
int length{0};
|
|
75
|
+
|
|
76
|
+
bool empty() const {
|
|
77
|
+
return length <= 0;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Where a selection started and where the pointer is now, for one paragraph.
|
|
82
|
+
//
|
|
83
|
+
// One per surface rather than one per paragraph: selecting inside a second
|
|
84
|
+
// `<Text>` clears the first, which is what a desktop does and what a single
|
|
85
|
+
// anchor enforces by construction.
|
|
86
|
+
class TextSelection {
|
|
87
|
+
public:
|
|
88
|
+
// A press at `index` inside the selectable paragraph `tag`, at `at` in the
|
|
89
|
+
// surface's coordinates. Nothing is selected yet; see the header.
|
|
90
|
+
void press(facebook::react::Tag tag, int index, facebook::react::Point at);
|
|
91
|
+
|
|
92
|
+
// The pointer has moved to `index` at `at`. Answers true the once, when the
|
|
93
|
+
// move claims the gesture: the caller cancels the touch sequence it was
|
|
94
|
+
// reporting and stops reporting moves.
|
|
95
|
+
//
|
|
96
|
+
// False, and no selection, until the pointer has travelled
|
|
97
|
+
// `kTextSelectionThreshold` from the press. False forever if the press was
|
|
98
|
+
// not inside selectable text.
|
|
99
|
+
bool moveTo(int index, facebook::react::Point at);
|
|
100
|
+
|
|
101
|
+
// The pointer is up. The selection stays on screen; what it stops is the
|
|
102
|
+
// dragging.
|
|
103
|
+
void release();
|
|
104
|
+
|
|
105
|
+
// Nothing is selected. A tap, a press in a different paragraph, a mutation
|
|
106
|
+
// that replaced the text: all of them end here.
|
|
107
|
+
void clear();
|
|
108
|
+
|
|
109
|
+
// Whether a drag is currently selecting rather than touching. The caller
|
|
110
|
+
// reports no touch moves while this is true.
|
|
111
|
+
bool dragging() const {
|
|
112
|
+
return claimed_ && pressed_;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Whether there is a selection to draw, which outlives the drag.
|
|
116
|
+
bool active() const {
|
|
117
|
+
return claimed_ && !range().empty();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
facebook::react::Tag tag() const {
|
|
121
|
+
return claimed_ ? tag_ : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The selected range, ordered: a drag to the left of its anchor selects the
|
|
125
|
+
// same text as a drag to the right.
|
|
126
|
+
TextSelectionRange range() const;
|
|
127
|
+
|
|
128
|
+
private:
|
|
129
|
+
facebook::react::Tag tag_{0};
|
|
130
|
+
int anchor_{0};
|
|
131
|
+
int focus_{0};
|
|
132
|
+
facebook::react::Point origin_{};
|
|
133
|
+
bool pressed_{false};
|
|
134
|
+
bool claimed_{false};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// --- Copying it ---------------------------------------------------------------
|
|
138
|
+
//
|
|
139
|
+
// The two places a copy is asked for are both outside the pointer path: a
|
|
140
|
+
// `Copy` role in a menu, which `performMenuRole` performs, and Ctrl or Cmd with
|
|
141
|
+
// C, which arrives at a window's key controller. Neither can reach the object
|
|
142
|
+
// holding the selection -- one is in the platform services and the other in the
|
|
143
|
+
// host's own main -- so the host leaves a way to ask here, which is the
|
|
144
|
+
// arrangement `setWindowBoundsListener` and `setEventListenerInstaller` use for
|
|
145
|
+
// the same reason.
|
|
146
|
+
//
|
|
147
|
+
// Set by whatever owns the selection: GtkTouchDispatcher, the AppKit mounting
|
|
148
|
+
// manager, the Win32 window. Null when no host has set one, which is every
|
|
149
|
+
// build that is not a host -- the tests and the portability probe.
|
|
150
|
+
void setSelectedTextProvider(std::function<std::string()> provider);
|
|
151
|
+
|
|
152
|
+
// What is selected, or empty when nothing is. Safe to call with no provider.
|
|
153
|
+
std::string selectedText();
|
|
154
|
+
|
|
155
|
+
// Puts it on the clipboard and says whether there was anything to put. False is
|
|
156
|
+
// the answer a `Copy` role wants when no paragraph has a selection: it means
|
|
157
|
+
// "ask the focused widget instead", which is what the role meant before
|
|
158
|
+
// paragraphs could be selected at all.
|
|
159
|
+
bool copySelectedText();
|
|
160
|
+
|
|
161
|
+
} // namespace basalt
|