basalt-core 0.0.0-rc5 → 0.1.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 (143) 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/packageApp.d.ts.map +1 -1
  6. package/dist/cli/packageApp.js +7 -1
  7. package/dist/cli/packageApp.js.map +1 -1
  8. package/dist/metro-config.d.ts.map +1 -1
  9. package/dist/metro-config.js +53 -5
  10. package/dist/metro-config.js.map +1 -1
  11. package/dist/src/KeyHandler.d.ts +1 -1
  12. package/dist/src/KeyHandler.d.ts.map +1 -1
  13. package/dist/src/KeyHandler.js.map +1 -1
  14. package/dist/src/index.d.ts +5 -0
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/overrides/Alert.d.ts +1 -1
  17. package/dist/src/overrides/Alert.js +1 -1
  18. package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
  19. package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
  20. package/dist/src/overrides/BaseViewConfig.js +21 -3
  21. package/dist/src/overrides/BaseViewConfig.js.map +1 -1
  22. package/dist/src/overrides/Button.d.ts +81 -0
  23. package/dist/src/overrides/Button.d.ts.map +1 -0
  24. package/dist/src/overrides/Button.js +109 -0
  25. package/dist/src/overrides/Button.js.map +1 -0
  26. package/dist/src/overrides/ScrollView.d.ts +25 -2
  27. package/dist/src/overrides/ScrollView.d.ts.map +1 -1
  28. package/dist/src/overrides/ScrollView.js +64 -7
  29. package/dist/src/overrides/ScrollView.js.map +1 -1
  30. package/dist/src/overrides/Settings.d.ts +81 -0
  31. package/dist/src/overrides/Settings.d.ts.map +1 -0
  32. package/dist/src/overrides/Settings.js +120 -0
  33. package/dist/src/overrides/Settings.js.map +1 -0
  34. package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
  35. package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
  36. package/dist/src/overrides/processDecelerationRate.js +31 -0
  37. package/dist/src/overrides/processDecelerationRate.js.map +1 -0
  38. package/dist/src/useContextMenu.d.ts +57 -6
  39. package/dist/src/useContextMenu.d.ts.map +1 -1
  40. package/dist/src/useContextMenu.js +34 -1
  41. package/dist/src/useContextMenu.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/native/CMakeLists.txt +51 -0
  44. package/native/core/ActionSheet.cpp +75 -0
  45. package/native/core/ActionSheet.h +90 -0
  46. package/native/core/AppStateHost.cpp +76 -0
  47. package/native/core/AppStateHost.h +68 -0
  48. package/native/core/BackgroundLayers.h +282 -0
  49. package/native/core/BlendModes.h +67 -0
  50. package/native/core/CoreModules.cpp +380 -11
  51. package/native/core/CoreModules.h +140 -4
  52. package/native/core/CrashHandler.cpp +246 -0
  53. package/native/core/CrashHandler.h +65 -0
  54. package/native/core/CursorNames.h +110 -0
  55. package/native/core/DevMenu.cpp +6 -5
  56. package/native/core/DeviceMetrics.cpp +87 -0
  57. package/native/core/DeviceMetrics.h +80 -0
  58. package/native/core/Filters.h +336 -0
  59. package/native/core/FontFitting.h +183 -0
  60. package/native/core/FontScaling.h +153 -0
  61. package/native/core/FontVariants.h +148 -0
  62. package/native/core/Gradients.h +523 -0
  63. package/native/core/ImageAnimation.h +109 -0
  64. package/native/core/ImageBytes.cpp +28 -1
  65. package/native/core/ImageBytes.h +17 -1
  66. package/native/core/ImageProgress.h +85 -0
  67. package/native/core/LabelRegistry.h +189 -0
  68. package/native/core/LegacyShadow.h +90 -0
  69. package/native/core/LiveRegions.h +114 -0
  70. package/native/core/MenuModule.cpp +35 -4
  71. package/native/core/MountingWalk.h +125 -2
  72. package/native/core/PlatformServices.h +187 -3
  73. package/native/core/SafeAreaComponent.cpp +115 -0
  74. package/native/core/SafeAreaComponent.h +105 -0
  75. package/native/core/ScreensComponent.cpp +201 -0
  76. package/native/core/ScreensComponent.h +216 -0
  77. package/native/core/ScrollBounds.cpp +8 -0
  78. package/native/core/ScrollBounds.h +14 -0
  79. package/native/core/ScrollIndicator.h +67 -0
  80. package/native/core/ScrollMomentum.cpp +24 -0
  81. package/native/core/ScrollMomentum.h +14 -0
  82. package/native/core/ScrollSnap.cpp +121 -12
  83. package/native/core/ScrollSnap.h +45 -0
  84. package/native/core/ScrollVisiblePosition.cpp +68 -0
  85. package/native/core/ScrollVisiblePosition.h +101 -0
  86. package/native/core/SettingsStore.cpp +199 -0
  87. package/native/core/SettingsStore.h +127 -0
  88. package/native/core/ShadowShape.h +38 -0
  89. package/native/core/SystemLocale.cpp +103 -0
  90. package/native/core/SystemLocale.h +86 -0
  91. package/native/core/TestDialog.cpp +45 -6
  92. package/native/core/TestDialog.h +15 -0
  93. package/native/core/TextAlignments.h +240 -0
  94. package/native/core/TextChecking.h +133 -0
  95. package/native/core/TextColors.h +83 -0
  96. package/native/core/TextDecorations.h +101 -0
  97. package/native/core/TextDirection.cpp +242 -0
  98. package/native/core/TextDirection.h +88 -0
  99. package/native/core/TextHighlight.h +30 -0
  100. package/native/core/TextSelection.cpp +97 -0
  101. package/native/core/TextSelection.h +161 -0
  102. package/native/core/TextShadows.h +96 -0
  103. package/native/core/TextVerticalAlign.h +33 -0
  104. package/native/core/UIManagerAccess.cpp +36 -0
  105. package/native/core/UIManagerAccess.h +28 -0
  106. package/native/core/WindowBoundsCache.cpp +58 -1
  107. package/native/core/WindowControl.h +24 -0
  108. package/native/core/WritingDirections.h +52 -0
  109. package/native/core/portability_probe.cpp +7 -0
  110. package/native/tests/RenamedFontFile.h +161 -0
  111. package/native/tests/TestHarness.cpp +18 -0
  112. package/native/tests/test_action_sheet.cpp +157 -0
  113. package/native/tests/test_app_state.cpp +60 -0
  114. package/native/tests/test_background_layers.cpp +281 -0
  115. package/native/tests/test_blend_modes.cpp +79 -0
  116. package/native/tests/test_blobs.cpp +1 -1
  117. package/native/tests/test_device_metrics.cpp +126 -0
  118. package/native/tests/test_filters.cpp +271 -0
  119. package/native/tests/test_font_fitting.cpp +172 -0
  120. package/native/tests/test_font_scaling.cpp +127 -0
  121. package/native/tests/test_font_variants.cpp +84 -0
  122. package/native/tests/test_gradients.cpp +390 -0
  123. package/native/tests/test_image_animation.cpp +105 -0
  124. package/native/tests/test_image_progress.cpp +94 -0
  125. package/native/tests/test_label_registry.cpp +262 -0
  126. package/native/tests/test_legacy_shadow.cpp +165 -0
  127. package/native/tests/test_live_regions.cpp +108 -0
  128. package/native/tests/test_menu_entries.cpp +192 -0
  129. package/native/tests/test_momentum.cpp +45 -0
  130. package/native/tests/test_scroll_bounds.cpp +42 -0
  131. package/native/tests/test_scroll_indicator.cpp +61 -0
  132. package/native/tests/test_scroll_snap.cpp +129 -0
  133. package/native/tests/test_scroll_visible_position.cpp +121 -0
  134. package/native/tests/test_settings_store.cpp +257 -0
  135. package/native/tests/test_shadow_shape.cpp +47 -0
  136. package/native/tests/test_system_locale.cpp +36 -0
  137. package/native/tests/test_text_alignment.cpp +299 -0
  138. package/native/tests/test_text_checking.cpp +31 -0
  139. package/native/tests/test_text_colors.cpp +92 -0
  140. package/native/tests/test_text_decorations.cpp +100 -0
  141. package/native/tests/test_text_selection.cpp +168 -0
  142. package/native/tests/test_text_shadows.cpp +132 -0
  143. package/package.json +4 -2
@@ -0,0 +1,240 @@
1
+ // `textAlign`, resolved to an edge.
2
+ //
3
+ // React Native 0.87 added `start` and `end` beside `left` and `right`, and the
4
+ // two kinds are not the same question. `left` is the left of the box. `end` is
5
+ // the edge a line of text *finishes* at, which in a right-to-left paragraph is
6
+ // the left one -- so a paragraph with `writingDirection: 'rtl'` and
7
+ // `textAlign: 'end'` is flush left, and all three hosts had it flush right.
8
+ //
9
+ // `natural`, which is the default, is the other relative one: the edge a line
10
+ // starts from.
11
+ //
12
+ // ## Why a table in core rather than three enum mappings
13
+ //
14
+ // Because it was three enum mappings, and the three disagreed. Measured on
15
+ // 2026-10-10, over a paragraph of Hebrew in a 300pt box, before this existed:
16
+ //
17
+ // | the app wrote | GTK | AppKit | Win32 |
18
+ // | rtl + end | right | right | left |
19
+ // | rtl + left | left | right | left |
20
+ // | hebrew + natural | right | left | left |
21
+ // | hebrew + left | right | left | left |
22
+ //
23
+ // Four rows, no two hosts agreeing on all of them, and one correct cell between
24
+ // them -- Win32's `end`, which is right because DirectWrite's alignments are
25
+ // themselves relative. Each host had made a local decision about one enum
26
+ // value, and the question is not local: it is "which edge, given a direction",
27
+ // asked three times.
28
+ //
29
+ // So the question is answered here, and what is left per host is the spelling:
30
+ // a `PangoAlignment`, a Core Text flush factor, a `DWRITE_TEXT_ALIGNMENT`.
31
+ // `core/TextDirection.h` answers the direction half.
32
+
33
+ #pragma once
34
+
35
+ #include "TextVerticalAlign.h"
36
+
37
+ #include <react/renderer/attributedstring/AttributedString.h>
38
+ #include <react/renderer/attributedstring/TextAttributes.h>
39
+
40
+ #include <optional>
41
+
42
+ namespace basalt {
43
+
44
+ // Where the text sits in the box, with nothing relative left in it.
45
+ //
46
+ // `Justified` is here because React Native's enum has it, and it is not an edge
47
+ // at all: every host passes it straight to the engine, which stretches the
48
+ // lines and then puts the last one against the start edge. Kept in the enum so
49
+ // that a caller switching on this handles it rather than falling into a default
50
+ // that silently means left.
51
+ enum class PhysicalTextAlignment { Left, Center, Right, Justified };
52
+
53
+ // The two directions a paragraph has, which are not the same question.
54
+ //
55
+ // `paragraph` is which way the text runs: the `writingDirection` prop, or
56
+ // Unicode's P2 over the text when the app said `natural` or said nothing. It
57
+ // decides where an unaligned paragraph sits, because `natural` means "the edge
58
+ // this text starts from".
59
+ //
60
+ // `relative` is what the words `start` and `end` mean, and that is the *app's*
61
+ // layout direction rather than the text's. A mirrored app lays every other
62
+ // relative thing out that way -- Yoga resolves `paddingStart` and
63
+ // `marginEnd` against `LayoutConstraints::layoutDirection` -- and a paragraph
64
+ // whose `textAlign: 'start'` pointed the other way from its own
65
+ // `paddingStart` would be the one thing in the window facing backwards.
66
+ //
67
+ // Upstream resolves it the same way and in one place:
68
+ // `RCTResolveTextAlignment` in `RCTAttributedTextUtils.mm` takes
69
+ // `(textAlignment, layoutDirection)` and answers `Start` with the layout's
70
+ // left or right. The direction reaches here in
71
+ // `TextAttributes::layoutDirection`, which `ParagraphShadowNode::getContent`
72
+ // fills from `YGNodeLayoutGetDirection` -- so it is Yoga's answer for this
73
+ // paragraph, not a global, and it is right for a subtree an app mirrored on
74
+ // its own.
75
+ //
76
+ // Upstream's iOS half also mirrors `left` and `right` themselves when the
77
+ // layout is right to left. This platform does not, and backlog/text.md records
78
+ // that with the measurement: `left` is the left of the box here, on all three
79
+ // hosts, which is the rule the rest of this file is written to.
80
+ struct TextDirections {
81
+ bool paragraph = false;
82
+ bool relative = false;
83
+ };
84
+
85
+ // The edge for `alignment`, given what the text does and what the layout does.
86
+ //
87
+ // No alignment at all is `natural`, which is React Native's own default and the
88
+ // reason this takes an optional: a paragraph that said nothing still has an
89
+ // answer, and in a right-to-left one it is not the left edge.
90
+ inline PhysicalTextAlignment physicalTextAlignment(
91
+ std::optional<facebook::react::TextAlignment> alignment,
92
+ TextDirections directions) {
93
+ const bool rightToLeft = directions.paragraph;
94
+ if (!alignment.has_value()) {
95
+ return rightToLeft ? PhysicalTextAlignment::Right : PhysicalTextAlignment::Left;
96
+ }
97
+
98
+ switch (*alignment) {
99
+ case facebook::react::TextAlignment::Left:
100
+ return PhysicalTextAlignment::Left;
101
+ case facebook::react::TextAlignment::Right:
102
+ return PhysicalTextAlignment::Right;
103
+ case facebook::react::TextAlignment::Center:
104
+ return PhysicalTextAlignment::Center;
105
+ case facebook::react::TextAlignment::Justified:
106
+ return PhysicalTextAlignment::Justified;
107
+ #if BASALT_RN_MINOR >= 87
108
+ // The two that arrived in 0.87, and the whole reason this file exists.
109
+ // Against the layout rather than the text: see TextDirections above.
110
+ case facebook::react::TextAlignment::Start:
111
+ return directions.relative ? PhysicalTextAlignment::Right : PhysicalTextAlignment::Left;
112
+ case facebook::react::TextAlignment::End:
113
+ return directions.relative ? PhysicalTextAlignment::Left : PhysicalTextAlignment::Right;
114
+ #endif
115
+ case facebook::react::TextAlignment::Natural:
116
+ break;
117
+ }
118
+ return rightToLeft ? PhysicalTextAlignment::Right : PhysicalTextAlignment::Left;
119
+ }
120
+
121
+ // The edge's name, for the tree dump the three hosts compare line by line.
122
+ //
123
+ // The resolved edge rather than the prop, which is the point: `textAlign: 'end'`
124
+ // is the same word on every host and a different edge depending on the
125
+ // paragraph's direction, so printing the prop would compare the question rather
126
+ // than the answer. Nothing else in a dump shows it -- a right-aligned paragraph
127
+ // has the same box and the same string as a left-aligned one.
128
+ inline const char *physicalTextAlignmentName(PhysicalTextAlignment alignment) {
129
+ switch (alignment) {
130
+ case PhysicalTextAlignment::Center:
131
+ return "center";
132
+ case PhysicalTextAlignment::Right:
133
+ return "right";
134
+ case PhysicalTextAlignment::Justified:
135
+ return "justified";
136
+ case PhysicalTextAlignment::Left:
137
+ break;
138
+ }
139
+ return "left";
140
+ }
141
+
142
+ // The name for a paragraph, which is the question a mounting manager has: it
143
+ // holds the attributed string and wants the line to print.
144
+ const char *paragraphTextAlignmentName(const facebook::react::AttributedString &attributedString);
145
+
146
+ // How far along the line's own free space the text sits: 0 for the left edge,
147
+ // 0.5 for the middle, 1 for the right.
148
+ //
149
+ // Core Text's `CTLineGetPenOffsetForFlush` takes exactly this, and it is what
150
+ // the AppKit layer draws with -- it lays out its own lines so that
151
+ // `numberOfLines` can be honoured, which means the frame never applies the
152
+ // alignment and the offset has to be computed. Justified text is flush with the
153
+ // start edge, which is where its last line goes.
154
+ inline double textFlushFactor(PhysicalTextAlignment alignment, bool rightToLeft) {
155
+ switch (alignment) {
156
+ case PhysicalTextAlignment::Center:
157
+ return 0.5;
158
+ case PhysicalTextAlignment::Right:
159
+ return 1.0;
160
+ case PhysicalTextAlignment::Justified:
161
+ return rightToLeft ? 1.0 : 0.0;
162
+ case PhysicalTextAlignment::Left:
163
+ break;
164
+ }
165
+ return 0.0;
166
+ }
167
+
168
+ // --- The other axis ----------------------------------------------------------
169
+ //
170
+ // `textAlignVertical` puts the paragraph somewhere in its own box: `top`,
171
+ // `bottom`, `center` or `auto`. `verticalAlign` is the cross-platform spelling
172
+ // of the same thing and needs nothing of its own here -- React Native's
173
+ // `Text.js` and `TextInput.js` both rewrite it into `textAlignVertical` before
174
+ // the props are sent, with `middle` becoming `center`. Measured on 2026-10-10;
175
+ // the backlog said the cross-platform name was dropped before any platform saw
176
+ // it, which was true of ReactCommon and not of the JavaScript above it.
177
+ //
178
+ // `auto` is the top, which is what every engine does with no instruction and
179
+ // what React Native's own default produces.
180
+ //
181
+ // Only reachable when the box is taller than the text, which for a paragraph
182
+ // means a `<Text>` with a height of its own: a paragraph that Yoga sized to its
183
+ // own content has nothing to be aligned inside.
184
+
185
+ // What the app asked for, for the tree dump: "auto", "top", "center",
186
+ // "bottom", or nullptr when it said nothing.
187
+ //
188
+ // The prop rather than the offset it produces, which is the opposite of the
189
+ // horizontal line above and for a reason: the edge a line sits against is the
190
+ // same word on every host and a different edge depending on the direction, so
191
+ // printing the prop there would compare the question. Here the prop *is* the
192
+ // answer, and the offset depends on the box, which differs between hosts by a
193
+ // line's worth of font metrics. What this line proves is the whole chain from
194
+ // JavaScript -- `verticalAlign` is rewritten into `textAlignVertical` by React
195
+ // Native's own `Text.js`, and nothing else here can show that it arrived.
196
+ inline const char *textAlignVerticalName(
197
+ std::optional<facebook::react::TextAlignmentVertical> alignment) {
198
+ if (!alignment.has_value()) {
199
+ return nullptr;
200
+ }
201
+ switch (*alignment) {
202
+ case facebook::react::TextAlignmentVertical::Auto:
203
+ return "auto";
204
+ case facebook::react::TextAlignmentVertical::Top:
205
+ return "top";
206
+ case facebook::react::TextAlignmentVertical::Center:
207
+ return "center";
208
+ case facebook::react::TextAlignmentVertical::Bottom:
209
+ return "bottom";
210
+ }
211
+ return nullptr;
212
+ }
213
+
214
+ // How far down its box the paragraph sits: 0 for the top, 0.5 for the middle,
215
+ // 1 for the bottom. The same shape as the horizontal flush factor above, and
216
+ // for the same reason -- it is the one number each of the three hosts can use,
217
+ // over a Pango translation, a Core Text origin and a DirectWrite draw origin.
218
+ inline double textVerticalFlushFactor(
219
+ std::optional<facebook::react::TextAlignmentVertical> alignment) {
220
+ if (!alignment.has_value()) {
221
+ return 0.0;
222
+ }
223
+ switch (*alignment) {
224
+ case facebook::react::TextAlignmentVertical::Center:
225
+ return 0.5;
226
+ case facebook::react::TextAlignmentVertical::Bottom:
227
+ return 1.0;
228
+ case facebook::react::TextAlignmentVertical::Top:
229
+ case facebook::react::TextAlignmentVertical::Auto:
230
+ break;
231
+ }
232
+ return 0.0;
233
+ }
234
+
235
+ // The offset that factor produces, given a box and the text in it, is
236
+ // `textVerticalOffset` in core/TextVerticalAlign.h -- a separate header because
237
+ // each host's widget layer needs it and has no React Native in it. The box is
238
+ // only known while drawing, so that is where it is asked.
239
+
240
+ } // namespace basalt
@@ -0,0 +1,133 @@
1
+ // `spellCheck` and `autoCorrect`, as three states rather than two.
2
+ //
3
+ // Both are `std::optional<bool>` in React Native's text-input traits, and the
4
+ // third state is the one that matters: **unset is not false.** A field that says
5
+ // nothing about spell checking wants whatever the platform does by default,
6
+ // which on a Mac is to check and on Linux is whatever the input method was
7
+ // configured to do. Resolving unset to `false` would turn every ordinary field
8
+ // into one that had opted out, and the app never said so.
9
+ //
10
+ // So the hosts are given three states and leave the toolkit alone for the third.
11
+ // That is the same shape the accessible flags use, for the same reason: React
12
+ // Native's optionals carry "did not say", and a platform default is a real
13
+ // answer rather than a missing one.
14
+ //
15
+ // ## What each toolkit can honour, which is not the same pair
16
+ //
17
+ // - AppKit has both, on an NSTextView: `continuousSpellCheckingEnabled` and
18
+ // `automaticSpellingCorrectionEnabled`. A single-line NSTextField has
19
+ // neither of its own -- the field editor it borrows while focused is the
20
+ // NSTextView that does -- which is why the peer applies them again when a
21
+ // field takes focus.
22
+ // - GTK has spell checking as an input *hint*, `GTK_INPUT_HINT_SPELLCHECK`
23
+ // and its negative, which is a suggestion to the input method rather than
24
+ // an instruction to a checker. It has no hint for autocorrection at all:
25
+ // `WORD_COMPLETION` is completion, which is a different offer.
26
+ //
27
+ // backlog/platform-macos.md and backlog/text.md record the halves that are not
28
+ // a call on each platform.
29
+
30
+ #pragma once
31
+
32
+ #include <react/renderer/components/iostextinput/primitives.h>
33
+
34
+ #include <optional>
35
+
36
+ namespace basalt {
37
+
38
+ // What an app asked for, with "did not say" kept.
39
+ enum class TextCheckingFlag {
40
+ Unset,
41
+ On,
42
+ Off,
43
+ };
44
+
45
+ inline TextCheckingFlag textCheckingFlag(std::optional<bool> asked) {
46
+ if (!asked.has_value()) {
47
+ return TextCheckingFlag::Unset;
48
+ }
49
+ return *asked ? TextCheckingFlag::On : TextCheckingFlag::Off;
50
+ }
51
+
52
+ // Its name, for a tree dump: nullptr when the app said nothing, so the line is
53
+ // absent rather than claiming a default the app did not ask for.
54
+ inline const char *textCheckingName(TextCheckingFlag flag) {
55
+ switch (flag) {
56
+ case TextCheckingFlag::On:
57
+ return "on";
58
+ case TextCheckingFlag::Off:
59
+ return "off";
60
+ case TextCheckingFlag::Unset:
61
+ return nullptr;
62
+ }
63
+ return nullptr;
64
+ }
65
+
66
+ // `autoCapitalize` and `keyboardType`, which are the other half of the same
67
+ // entry and the other way round: GTK can express both and macOS neither.
68
+ //
69
+ // Both are plain enums rather than optionals, so there is no "did not say"
70
+ // here: React Native's default for capitalisation **is** sentences, which is
71
+ // what iOS does, and a host that treated the default as "no opinion" would be
72
+ // quietly disagreeing with every other platform. So the hint is set either way
73
+ // and the comment above each mapping says what it means.
74
+ //
75
+ // Nothing resolves them in core beyond naming them for the dump: a GTK input
76
+ // purpose and a GTK input hint are GTK's vocabulary, and AppKit has no
77
+ // vocabulary to translate into. What is shared is the word both dumps print.
78
+
79
+ // `autoCapitalize`, as React Native spells it in JavaScript.
80
+ inline const char *autoCapitalizeName(
81
+ facebook::react::AutocapitalizationType type) {
82
+ switch (type) {
83
+ case facebook::react::AutocapitalizationType::None:
84
+ return "none";
85
+ case facebook::react::AutocapitalizationType::Words:
86
+ return "words";
87
+ case facebook::react::AutocapitalizationType::Sentences:
88
+ return "sentences";
89
+ case facebook::react::AutocapitalizationType::Characters:
90
+ return "characters";
91
+ }
92
+ return "sentences";
93
+ }
94
+
95
+ // `keyboardType`, as React Native spells it. The iOS-only and Android-only
96
+ // members are named as they are written in a stylesheet rather than collapsed,
97
+ // because a dump that said "default" for four different asks would hide which
98
+ // one an app made.
99
+ inline const char *keyboardTypeName(facebook::react::KeyboardType type) {
100
+ switch (type) {
101
+ case facebook::react::KeyboardType::Default:
102
+ return "default";
103
+ case facebook::react::KeyboardType::EmailAddress:
104
+ return "email-address";
105
+ case facebook::react::KeyboardType::Numeric:
106
+ return "numeric";
107
+ case facebook::react::KeyboardType::PhonePad:
108
+ return "phone-pad";
109
+ case facebook::react::KeyboardType::NumberPad:
110
+ return "number-pad";
111
+ case facebook::react::KeyboardType::DecimalPad:
112
+ return "decimal-pad";
113
+ case facebook::react::KeyboardType::ASCIICapable:
114
+ return "ascii-capable";
115
+ case facebook::react::KeyboardType::NumbersAndPunctuation:
116
+ return "numbers-and-punctuation";
117
+ case facebook::react::KeyboardType::URL:
118
+ return "url";
119
+ case facebook::react::KeyboardType::NamePhonePad:
120
+ return "name-phone-pad";
121
+ case facebook::react::KeyboardType::Twitter:
122
+ return "twitter";
123
+ case facebook::react::KeyboardType::WebSearch:
124
+ return "web-search";
125
+ case facebook::react::KeyboardType::ASCIICapableNumberPad:
126
+ return "ascii-capable-number-pad";
127
+ case facebook::react::KeyboardType::VisiblePassword:
128
+ return "visible-password";
129
+ }
130
+ return "default";
131
+ }
132
+
133
+ } // namespace basalt
@@ -0,0 +1,83 @@
1
+ // A fragment's foreground and background, with `opacity` applied.
2
+ //
3
+ // Three rules that were spelled twice, once per host, and one of them was not
4
+ // spelled at all:
5
+ //
6
+ // - the default foreground is **opaque black**, which is React Native's and
7
+ // not the platform's. AppKit's `labelColor` is white in dark mode and would
8
+ // make default text invisible on a light background; matching React Native
9
+ // is what keeps the same app looking the same on all three desktops.
10
+ // - `opacity` multiplies the alpha of both colours. No host read it, so a
11
+ // `<Text style={{opacity: 0.5}}>` inside another `<Text>` was fully opaque.
12
+ // - a background with no colour is not transparent black, it is *nothing*:
13
+ // drawing it would paint a box behind text that asked for none.
14
+ //
15
+ // Copied from upstream's `RCTEffectiveForegroundColorFromTextAttributes` and its
16
+ // background counterpart, including the detail that opacity applies to the
17
+ // default black as well: an app that sets opacity and no colour still gets
18
+ // translucent text.
19
+
20
+ #pragma once
21
+
22
+ #include <react/renderer/attributedstring/TextAttributes.h>
23
+ #include <react/renderer/graphics/Color.h>
24
+
25
+ #include <cmath>
26
+ #include <optional>
27
+
28
+ namespace basalt {
29
+
30
+ // A straight sRGB colour, which is what both toolkits want handing over.
31
+ struct TextColor {
32
+ float red{0.0F};
33
+ float green{0.0F};
34
+ float blue{0.0F};
35
+ float alpha{1.0F};
36
+ };
37
+
38
+ // `opacity`, as a factor. NaN is React Native's "unset" and means 1; a value
39
+ // outside 0..1 is clamped, a stylesheet being able to ask for either.
40
+ inline float textOpacity(const facebook::react::TextAttributes &textAttributes) {
41
+ if (std::isnan(textAttributes.opacity)) {
42
+ return 1.0F;
43
+ }
44
+ const float opacity = static_cast<float>(textAttributes.opacity);
45
+ if (opacity < 0.0F) {
46
+ return 0.0F;
47
+ }
48
+ return opacity > 1.0F ? 1.0F : opacity;
49
+ }
50
+
51
+ // The colour the glyphs are drawn in.
52
+ inline TextColor textForegroundColor(
53
+ const facebook::react::TextAttributes &textAttributes) {
54
+ TextColor color;
55
+ if (textAttributes.foregroundColor) {
56
+ const auto components =
57
+ facebook::react::colorComponentsFromColor(textAttributes.foregroundColor);
58
+ color.red = components.red;
59
+ color.green = components.green;
60
+ color.blue = components.blue;
61
+ color.alpha = components.alpha;
62
+ }
63
+ color.alpha *= textOpacity(textAttributes);
64
+ return color;
65
+ }
66
+
67
+ // The colour behind them, or nothing.
68
+ inline std::optional<TextColor> textBackgroundColor(
69
+ const facebook::react::TextAttributes &textAttributes) {
70
+ if (!textAttributes.backgroundColor) {
71
+ return std::nullopt;
72
+ }
73
+ const auto components =
74
+ facebook::react::colorComponentsFromColor(textAttributes.backgroundColor);
75
+ TextColor color;
76
+ color.red = components.red;
77
+ color.green = components.green;
78
+ color.blue = components.blue;
79
+ color.alpha = components.alpha * textOpacity(textAttributes);
80
+ return color;
81
+ }
82
+
83
+ } // namespace basalt
@@ -0,0 +1,101 @@
1
+ // `textDecorationLine`, `textDecorationColor` and `textDecorationStyle`.
2
+ //
3
+ // The line was read on both hosts and the other two were not, so an underline
4
+ // was always a solid one in the text's own colour. What is shared here is the
5
+ // resolution rather than the drawing: which lines are on, what colour they are
6
+ // when an app names one, and the style it asked for. Each toolkit then maps the
7
+ // style to its own vocabulary, and they do not have the same one.
8
+ //
9
+ // ## What each toolkit can draw, which is not the same list
10
+ //
11
+ // React Native has five styles -- solid, double, dotted, dashed and wavy -- and
12
+ // neither desktop has all five:
13
+ //
14
+ // - Core Text takes a style and a *pattern* in one bitmask, so
15
+ // `NSUnderlineStyleSingle | NSUnderlineStylePatternDot` is a dotted
16
+ // underline and `...PatternDash` a dashed one. It has no wavy.
17
+ // - Pango's underline is an enum with no patterns in it:
18
+ // `PANGO_UNDERLINE_SINGLE`, `DOUBLE` and `ERROR`, the last being the wavy
19
+ // red one a spell checker draws. So dotted and dashed have no value, and
20
+ // strikethrough is a boolean with no style at all.
21
+ //
22
+ // So each host draws what it has and falls back to a single line otherwise,
23
+ // which backlog/text.md records with the call that would close the gap: drawing
24
+ // the line in the snapshot rather than asking the text engine for it.
25
+ //
26
+ // Falling back rather than refusing is the choice worth stating. A dotted
27
+ // underline drawn solid is wrong in a way a reader can see; a dotted underline
28
+ // not drawn at all is wrong in a way that looks like the prop being ignored,
29
+ // which is what the three props were doing before any of this.
30
+
31
+ #pragma once
32
+
33
+ #include <react/renderer/attributedstring/TextAttributes.h>
34
+ #include <react/renderer/graphics/Color.h>
35
+
36
+ #include <optional>
37
+
38
+ namespace basalt {
39
+
40
+ // One fragment's decoration, in the terms both hosts take.
41
+ struct TextDecoration {
42
+ bool underline{false};
43
+ bool strikethrough{false};
44
+ facebook::react::TextDecorationStyle style{facebook::react::TextDecorationStyle::Solid};
45
+ // Unset means the text's own colour, which is what both toolkits do when no
46
+ // decoration colour is given, and is React Native's default: iOS sets
47
+ // `NSUnderlineColorAttributeName` only when `textDecorationColor` is there.
48
+ bool hasColor{false};
49
+ float red{0.0F};
50
+ float green{0.0F};
51
+ float blue{0.0F};
52
+ float alpha{0.0F};
53
+ };
54
+
55
+ // What a fragment decorates, or nothing.
56
+ //
57
+ // `textDecorationLine` is the prop that decides whether there is a decoration at
58
+ // all: a colour or a style with no line is an app styling something it did not
59
+ // ask for, and drawing a line there would be inventing one.
60
+ inline std::optional<TextDecoration> textDecoration(
61
+ const facebook::react::TextAttributes &textAttributes) {
62
+ using facebook::react::TextDecorationLineType;
63
+
64
+ if (!textAttributes.textDecorationLineType.has_value()) {
65
+ return std::nullopt;
66
+ }
67
+
68
+ TextDecoration decoration;
69
+ switch (*textAttributes.textDecorationLineType) {
70
+ case TextDecorationLineType::None:
71
+ return std::nullopt;
72
+ case TextDecorationLineType::Underline:
73
+ decoration.underline = true;
74
+ break;
75
+ case TextDecorationLineType::Strikethrough:
76
+ decoration.strikethrough = true;
77
+ break;
78
+ case TextDecorationLineType::UnderlineStrikethrough:
79
+ decoration.underline = true;
80
+ decoration.strikethrough = true;
81
+ break;
82
+ }
83
+
84
+ if (textAttributes.textDecorationStyle.has_value()) {
85
+ decoration.style = *textAttributes.textDecorationStyle;
86
+ }
87
+
88
+ if (textAttributes.textDecorationColor) {
89
+ const auto components =
90
+ facebook::react::colorComponentsFromColor(textAttributes.textDecorationColor);
91
+ decoration.hasColor = true;
92
+ decoration.red = components.red;
93
+ decoration.green = components.green;
94
+ decoration.blue = components.blue;
95
+ decoration.alpha = components.alpha;
96
+ }
97
+
98
+ return decoration;
99
+ }
100
+
101
+ } // namespace basalt