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.
- 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/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 +51 -0
- 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/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,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
|