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.
Files changed (149) hide show
  1. package/dist/cli/basalt.d.ts.map +1 -1
  2. package/dist/cli/basalt.js +18 -1
  3. package/dist/cli/basalt.js.map +1 -1
  4. package/dist/cli/bundle-cli.js +0 -0
  5. package/dist/cli/desktop.d.ts.map +1 -1
  6. package/dist/cli/desktop.js +53 -4
  7. package/dist/cli/desktop.js.map +1 -1
  8. package/dist/cli/packageApp.d.ts.map +1 -1
  9. package/dist/cli/packageApp.js +7 -1
  10. package/dist/cli/packageApp.js.map +1 -1
  11. package/dist/metro-config.d.ts.map +1 -1
  12. package/dist/metro-config.js +53 -5
  13. package/dist/metro-config.js.map +1 -1
  14. package/dist/src/KeyHandler.d.ts +1 -1
  15. package/dist/src/KeyHandler.d.ts.map +1 -1
  16. package/dist/src/KeyHandler.js.map +1 -1
  17. package/dist/src/index.d.ts +5 -0
  18. package/dist/src/index.d.ts.map +1 -1
  19. package/dist/src/overrides/Alert.d.ts +1 -1
  20. package/dist/src/overrides/Alert.js +1 -1
  21. package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
  22. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
  23. package/dist/src/overrides/BaseViewConfig.js +21 -3
  24. package/dist/src/overrides/BaseViewConfig.js.map +1 -1
  25. package/dist/src/overrides/Button.d.ts +81 -0
  26. package/dist/src/overrides/Button.d.ts.map +1 -0
  27. package/dist/src/overrides/Button.js +109 -0
  28. package/dist/src/overrides/Button.js.map +1 -0
  29. package/dist/src/overrides/ScrollView.d.ts +25 -2
  30. package/dist/src/overrides/ScrollView.d.ts.map +1 -1
  31. package/dist/src/overrides/ScrollView.js +64 -7
  32. package/dist/src/overrides/ScrollView.js.map +1 -1
  33. package/dist/src/overrides/Settings.d.ts +81 -0
  34. package/dist/src/overrides/Settings.d.ts.map +1 -0
  35. package/dist/src/overrides/Settings.js +120 -0
  36. package/dist/src/overrides/Settings.js.map +1 -0
  37. package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
  38. package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
  39. package/dist/src/overrides/processDecelerationRate.js +31 -0
  40. package/dist/src/overrides/processDecelerationRate.js.map +1 -0
  41. package/dist/src/useContextMenu.d.ts +57 -6
  42. package/dist/src/useContextMenu.d.ts.map +1 -1
  43. package/dist/src/useContextMenu.js +34 -1
  44. package/dist/src/useContextMenu.js.map +1 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/native/CMakeLists.txt +85 -1
  47. package/native/cmake/Skia.cmake +29 -6
  48. package/native/core/ActionSheet.cpp +75 -0
  49. package/native/core/ActionSheet.h +90 -0
  50. package/native/core/AppStateHost.cpp +76 -0
  51. package/native/core/AppStateHost.h +68 -0
  52. package/native/core/BackgroundLayers.h +282 -0
  53. package/native/core/BlendModes.h +67 -0
  54. package/native/core/CoreModules.cpp +380 -11
  55. package/native/core/CoreModules.h +140 -4
  56. package/native/core/CrashHandler.cpp +246 -0
  57. package/native/core/CrashHandler.h +65 -0
  58. package/native/core/CursorNames.h +110 -0
  59. package/native/core/DevMenu.cpp +6 -5
  60. package/native/core/DeviceMetrics.cpp +87 -0
  61. package/native/core/DeviceMetrics.h +80 -0
  62. package/native/core/Filters.h +336 -0
  63. package/native/core/FontFitting.h +183 -0
  64. package/native/core/FontScaling.h +153 -0
  65. package/native/core/FontVariants.h +148 -0
  66. package/native/core/Gradients.h +523 -0
  67. package/native/core/ImageAnimation.h +109 -0
  68. package/native/core/ImageBytes.cpp +28 -1
  69. package/native/core/ImageBytes.h +17 -1
  70. package/native/core/ImageProgress.h +85 -0
  71. package/native/core/LabelRegistry.h +189 -0
  72. package/native/core/LegacyShadow.h +90 -0
  73. package/native/core/LiveRegions.h +114 -0
  74. package/native/core/MenuModule.cpp +35 -4
  75. package/native/core/MountingWalk.h +125 -2
  76. package/native/core/PlatformServices.h +187 -3
  77. package/native/core/SafeAreaComponent.cpp +115 -0
  78. package/native/core/SafeAreaComponent.h +105 -0
  79. package/native/core/ScreensComponent.cpp +201 -0
  80. package/native/core/ScreensComponent.h +216 -0
  81. package/native/core/ScrollBounds.cpp +8 -0
  82. package/native/core/ScrollBounds.h +14 -0
  83. package/native/core/ScrollIndicator.h +67 -0
  84. package/native/core/ScrollMomentum.cpp +24 -0
  85. package/native/core/ScrollMomentum.h +14 -0
  86. package/native/core/ScrollSnap.cpp +121 -12
  87. package/native/core/ScrollSnap.h +45 -0
  88. package/native/core/ScrollVisiblePosition.cpp +68 -0
  89. package/native/core/ScrollVisiblePosition.h +101 -0
  90. package/native/core/SettingsStore.cpp +199 -0
  91. package/native/core/SettingsStore.h +127 -0
  92. package/native/core/ShadowShape.h +38 -0
  93. package/native/core/SkiaViewComponent.cpp +11 -0
  94. package/native/core/SkiaViewComponent.h +50 -0
  95. package/native/core/SystemLocale.cpp +103 -0
  96. package/native/core/SystemLocale.h +86 -0
  97. package/native/core/TestDialog.cpp +45 -6
  98. package/native/core/TestDialog.h +15 -0
  99. package/native/core/TextAlignments.h +240 -0
  100. package/native/core/TextChecking.h +133 -0
  101. package/native/core/TextColors.h +83 -0
  102. package/native/core/TextDecorations.h +101 -0
  103. package/native/core/TextDirection.cpp +242 -0
  104. package/native/core/TextDirection.h +88 -0
  105. package/native/core/TextHighlight.h +30 -0
  106. package/native/core/TextSelection.cpp +97 -0
  107. package/native/core/TextSelection.h +161 -0
  108. package/native/core/TextShadows.h +96 -0
  109. package/native/core/TextVerticalAlign.h +33 -0
  110. package/native/core/UIManagerAccess.cpp +36 -0
  111. package/native/core/UIManagerAccess.h +28 -0
  112. package/native/core/WindowBoundsCache.cpp +58 -1
  113. package/native/core/WindowControl.h +24 -0
  114. package/native/core/WritingDirections.h +52 -0
  115. package/native/core/portability_probe.cpp +7 -0
  116. package/native/tests/RenamedFontFile.h +161 -0
  117. package/native/tests/TestHarness.cpp +18 -0
  118. package/native/tests/test_action_sheet.cpp +157 -0
  119. package/native/tests/test_app_state.cpp +60 -0
  120. package/native/tests/test_background_layers.cpp +281 -0
  121. package/native/tests/test_blend_modes.cpp +79 -0
  122. package/native/tests/test_blobs.cpp +1 -1
  123. package/native/tests/test_device_metrics.cpp +126 -0
  124. package/native/tests/test_filters.cpp +271 -0
  125. package/native/tests/test_font_fitting.cpp +172 -0
  126. package/native/tests/test_font_scaling.cpp +127 -0
  127. package/native/tests/test_font_variants.cpp +84 -0
  128. package/native/tests/test_gradients.cpp +390 -0
  129. package/native/tests/test_image_animation.cpp +105 -0
  130. package/native/tests/test_image_progress.cpp +94 -0
  131. package/native/tests/test_label_registry.cpp +262 -0
  132. package/native/tests/test_legacy_shadow.cpp +165 -0
  133. package/native/tests/test_live_regions.cpp +108 -0
  134. package/native/tests/test_menu_entries.cpp +192 -0
  135. package/native/tests/test_momentum.cpp +45 -0
  136. package/native/tests/test_scroll_bounds.cpp +42 -0
  137. package/native/tests/test_scroll_indicator.cpp +61 -0
  138. package/native/tests/test_scroll_snap.cpp +129 -0
  139. package/native/tests/test_scroll_visible_position.cpp +121 -0
  140. package/native/tests/test_settings_store.cpp +257 -0
  141. package/native/tests/test_shadow_shape.cpp +47 -0
  142. package/native/tests/test_system_locale.cpp +36 -0
  143. package/native/tests/test_text_alignment.cpp +299 -0
  144. package/native/tests/test_text_checking.cpp +31 -0
  145. package/native/tests/test_text_colors.cpp +92 -0
  146. package/native/tests/test_text_decorations.cpp +100 -0
  147. package/native/tests/test_text_selection.cpp +168 -0
  148. package/native/tests/test_text_shadows.cpp +132 -0
  149. 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