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,96 @@
|
|
|
1
|
+
// `textShadowColor`, `textShadowOffset` and `textShadowRadius`, resolved.
|
|
2
|
+
//
|
|
3
|
+
// Three props that every pre-CSS React Native title sets and that no host read.
|
|
4
|
+
// They are `TextAttributes`, so they arrive per *fragment* -- per `<Text>` inside
|
|
5
|
+
// a `<Text>` -- and neither text engine here can draw a different shadow per run:
|
|
6
|
+
// GTK appends one `PangoLayout` to the snapshot, and AppKit draws lines with
|
|
7
|
+
// `CTLineDraw`, which does not honour an `NSShadow` attribute the way AppKit's
|
|
8
|
+
// own text drawing does. So one shadow is taken for the paragraph.
|
|
9
|
+
//
|
|
10
|
+
// ## Which fragment wins, and why that is written down
|
|
11
|
+
//
|
|
12
|
+
// The first fragment that asks for a shadow. That is the same rule
|
|
13
|
+
// `borderStyle` follows for four sides that can only have one stroke, and it is
|
|
14
|
+
// a limit rather than a reading of the spec: `<Text>` with a shadow holding a
|
|
15
|
+
// `<Text>` with a different one draws the outer one twice. Recorded in
|
|
16
|
+
// backlog/text.md rather than approximated, because the alternative -- drawing
|
|
17
|
+
// each run separately -- costs the single-layout arrangement both hosts are
|
|
18
|
+
// built on.
|
|
19
|
+
//
|
|
20
|
+
// ## The radius is a standard deviation, as iOS means it
|
|
21
|
+
//
|
|
22
|
+
// React Native's iOS half puts `textShadowRadius` into `NSShadow.shadowBlurRadius`,
|
|
23
|
+
// which is the `CALayer.shadowRadius` kind of parameter: a gaussian standard
|
|
24
|
+
// deviation rather than CSS's blur radius, which is twice one. So AppKit passes
|
|
25
|
+
// it to `CGContextSetShadowWithColor` unchanged and GTK doubles it for
|
|
26
|
+
// `GskShadow`, which takes CSS's. That is the same conversion, in the same
|
|
27
|
+
// direction, as `filter: drop-shadow(...)`; see core/Filters.h.
|
|
28
|
+
|
|
29
|
+
#pragma once
|
|
30
|
+
|
|
31
|
+
#include <react/renderer/attributedstring/AttributedString.h>
|
|
32
|
+
|
|
33
|
+
#include <cmath>
|
|
34
|
+
#include <optional>
|
|
35
|
+
|
|
36
|
+
namespace basalt {
|
|
37
|
+
|
|
38
|
+
// One text shadow, in the terms both hosts take: an offset, the gaussian's
|
|
39
|
+
// standard deviation, and a straight sRGB colour.
|
|
40
|
+
struct TextShadow {
|
|
41
|
+
float dx{0.0F};
|
|
42
|
+
float dy{0.0F};
|
|
43
|
+
float standardDeviation{0.0F};
|
|
44
|
+
float red{0.0F};
|
|
45
|
+
float green{0.0F};
|
|
46
|
+
float blue{0.0F};
|
|
47
|
+
float alpha{0.0F};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// The shadow a paragraph draws, or nothing.
|
|
51
|
+
//
|
|
52
|
+
// A shadow needs a colour with some alpha in it: React Native's default is no
|
|
53
|
+
// colour at all, and a transparent one is a shadow an app asked to be invisible.
|
|
54
|
+
// An offset of nothing and a radius of nothing is also nothing -- a shadow
|
|
55
|
+
// exactly behind the glyphs with no blur cannot be seen -- which is worth
|
|
56
|
+
// skipping rather than drawing, because both hosts pay for a shadow pass.
|
|
57
|
+
inline std::optional<TextShadow> textShadow(
|
|
58
|
+
const facebook::react::AttributedString &attributedString) {
|
|
59
|
+
for (const auto &fragment : attributedString.getFragments()) {
|
|
60
|
+
const auto &attributes = fragment.textAttributes;
|
|
61
|
+
if (!attributes.textShadowColor) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const auto components =
|
|
65
|
+
facebook::react::colorComponentsFromColor(attributes.textShadowColor);
|
|
66
|
+
if (components.alpha <= 0.0F) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
TextShadow shadow;
|
|
71
|
+
if (attributes.textShadowOffset.has_value()) {
|
|
72
|
+
shadow.dx = static_cast<float>(attributes.textShadowOffset->width);
|
|
73
|
+
shadow.dy = static_cast<float>(attributes.textShadowOffset->height);
|
|
74
|
+
}
|
|
75
|
+
// NaN is how React Native spells "not set" for a Float, so a radius that is
|
|
76
|
+
// not a number is zero rather than a crash in a blur.
|
|
77
|
+
if (!std::isnan(attributes.textShadowRadius)) {
|
|
78
|
+
shadow.standardDeviation = static_cast<float>(attributes.textShadowRadius);
|
|
79
|
+
}
|
|
80
|
+
if (shadow.standardDeviation < 0.0F) {
|
|
81
|
+
shadow.standardDeviation = 0.0F;
|
|
82
|
+
}
|
|
83
|
+
if (shadow.dx == 0.0F && shadow.dy == 0.0F && shadow.standardDeviation == 0.0F) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
shadow.red = components.red;
|
|
88
|
+
shadow.green = components.green;
|
|
89
|
+
shadow.blue = components.blue;
|
|
90
|
+
shadow.alpha = components.alpha;
|
|
91
|
+
return shadow;
|
|
92
|
+
}
|
|
93
|
+
return std::nullopt;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} // namespace basalt
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Where a paragraph sits in a box taller than it is: the arithmetic half.
|
|
2
|
+
//
|
|
3
|
+
// Separate from `core/TextAlignments.h` for one reason, and it is the reason
|
|
4
|
+
// `FocusRing.h` and `ControlMetrics.h` are separate too: this is needed by each
|
|
5
|
+
// host's *widget* layer -- `RnView.cpp`, `RnTextLayout.mm`,
|
|
6
|
+
// `RnWin32TextLayout.cpp` -- and that layer has no React Native in it and
|
|
7
|
+
// cannot include a header that names `TextAlignmentVertical`. Each of those
|
|
8
|
+
// three draws the paragraph, and the box it is drawing into is only known then:
|
|
9
|
+
// a view resized without its props changing has new slack and the same text.
|
|
10
|
+
//
|
|
11
|
+
// So the enum lives in TextAlignments.h with the rest of the alignment
|
|
12
|
+
// decisions, and the one line all three draw with lives here.
|
|
13
|
+
|
|
14
|
+
#pragma once
|
|
15
|
+
|
|
16
|
+
namespace basalt {
|
|
17
|
+
|
|
18
|
+
// How far down to start drawing, given the box and what the text needs.
|
|
19
|
+
//
|
|
20
|
+
// Never negative: a paragraph taller than its box starts at the top, however it
|
|
21
|
+
// was aligned, because the alternative is text pushed out through the top edge
|
|
22
|
+
// where there is nothing to scroll it back. iOS and Android both do this, and a
|
|
23
|
+
// `numberOfLines` paragraph is the case -- the box is the lines that fit and the
|
|
24
|
+
// text is all of them.
|
|
25
|
+
inline double textVerticalOffset(double boxHeight, double textHeight, double flush) {
|
|
26
|
+
const double slack = boxHeight - textHeight;
|
|
27
|
+
if (slack <= 0.0 || flush <= 0.0) {
|
|
28
|
+
return 0.0;
|
|
29
|
+
}
|
|
30
|
+
return slack * flush;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
} // namespace basalt
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#include "UIManagerAccess.h"
|
|
2
2
|
|
|
3
|
+
#include <glog/logging.h>
|
|
4
|
+
|
|
5
|
+
#include <mutex>
|
|
6
|
+
|
|
3
7
|
namespace basalt {
|
|
4
8
|
|
|
5
9
|
namespace {
|
|
@@ -19,6 +23,38 @@ std::shared_ptr<facebook::react::UIManager> sharedUIManager() {
|
|
|
19
23
|
return held().lock();
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
bool reportMountedSurface(facebook::react::SurfaceId surfaceId) {
|
|
27
|
+
const auto uiManager = sharedUIManager();
|
|
28
|
+
if (!uiManager) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
// `visit` answers false without running the callback when the surface is not
|
|
32
|
+
// there, and says it is safe from any thread. See the header for why this
|
|
33
|
+
// question is the one that matters.
|
|
34
|
+
const bool registered = uiManager->getShadowTreeRegistry().visit(
|
|
35
|
+
surfaceId, [](const facebook::react::ShadowTree &) {});
|
|
36
|
+
if (!registered) {
|
|
37
|
+
// Every time, because this should be rare: it means a transaction was
|
|
38
|
+
// applied after its surface had gone, which is the race this guards. A run
|
|
39
|
+
// that logs this in quantity is saying the refusal is not rare, and a
|
|
40
|
+
// *healthy* run that logs it at all is saying the question is wrong.
|
|
41
|
+
LOG(WARNING) << "mount report refused: surface " << surfaceId
|
|
42
|
+
<< " is no longer registered";
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// Once per process, so that a suite can tell "the guard is letting mounts
|
|
46
|
+
// through" from "the guard refuses everything". Refusing everything is the
|
|
47
|
+
// failure this has no other way of noticing: nothing asserts that a mount
|
|
48
|
+
// hook ran, so an over-eager refusal would stop Reanimated silently and leave
|
|
49
|
+
// every test green. See docs/backlog/testing.md.
|
|
50
|
+
static std::once_flag reported;
|
|
51
|
+
std::call_once(reported, [surfaceId] {
|
|
52
|
+
LOG(INFO) << "mount reported to the UIManager, surface " << surfaceId;
|
|
53
|
+
});
|
|
54
|
+
uiManager->reportMount(surfaceId);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
22
58
|
namespace {
|
|
23
59
|
|
|
24
60
|
EventListenerInstaller &installer() {
|
|
@@ -24,6 +24,34 @@ namespace basalt {
|
|
|
24
24
|
void setSharedUIManager(std::weak_ptr<facebook::react::UIManager> uiManager);
|
|
25
25
|
std::shared_ptr<facebook::react::UIManager> sharedUIManager();
|
|
26
26
|
|
|
27
|
+
// `reportMount`, but only for a surface the UIManager still has, which is a
|
|
28
|
+
// workaround for an upstream use-after-free and not a tidying-up.
|
|
29
|
+
//
|
|
30
|
+
// `UIManager::mountHooks_` holds raw, non-owning pointers and unregistering is
|
|
31
|
+
// the owner's job. `Scheduler` registers one and `~Scheduler` does not take it
|
|
32
|
+
// out again, so from the moment `destroyReactInstance` writes
|
|
33
|
+
// `scheduler_ = nullptr` the list contains a freed `EventPerformanceLogger` --
|
|
34
|
+
// three statements before it tells a mounting manager anything, so the epoch
|
|
35
|
+
// guard in MountingWalk.h cannot help. A mount draining on the main queue in
|
|
36
|
+
// that gap used to walk the list and call a virtual method on freed memory.
|
|
37
|
+
// See docs/backlog/testing.md.
|
|
38
|
+
//
|
|
39
|
+
// What makes a probe enough: `stopAllSurfaces()` runs *before* the Scheduler is
|
|
40
|
+
// freed and takes the shadow trees out of the registry, so "the hook is
|
|
41
|
+
// dangling" always implies "the surface has gone". Asking whether the surface is
|
|
42
|
+
// still registered therefore refuses exactly the dangerous case, and the healthy
|
|
43
|
+
// case is unaffected because a live surface is still in there. It also skips a
|
|
44
|
+
// call that had nothing to report: with no root shadow node `reportMount` only
|
|
45
|
+
// tells every hook the tree unmounted.
|
|
46
|
+
//
|
|
47
|
+
// Not airtight, and it is worth being clear about that. The surface could in
|
|
48
|
+
// principle be stopped between the question and the call, which is a few
|
|
49
|
+
// instructions rather than three statements of teardown. The fix is one line
|
|
50
|
+
// upstream, in `~Scheduler`.
|
|
51
|
+
//
|
|
52
|
+
// Returns whether it reported.
|
|
53
|
+
bool reportMountedSurface(facebook::react::SurfaceId surfaceId);
|
|
54
|
+
|
|
27
55
|
// Watching every event Fabric dispatches.
|
|
28
56
|
//
|
|
29
57
|
// `Scheduler::addEventListener` is how a library sees events before the
|
|
@@ -166,22 +166,79 @@ WindowBounds lastKnownWindowBounds() {
|
|
|
166
166
|
return cache();
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
namespace {
|
|
170
|
+
|
|
171
|
+
std::function<void()> &dimensionsListener() {
|
|
172
|
+
static std::function<void()> value;
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
} // namespace
|
|
177
|
+
|
|
178
|
+
namespace {
|
|
179
|
+
|
|
180
|
+
// The main surface's size, which is the window minus whatever the desktop drew
|
|
181
|
+
// around it. See the header.
|
|
182
|
+
std::pair<double, double> &surfaceSize() {
|
|
183
|
+
static std::pair<double, double> value{0.0, 0.0};
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
} // namespace
|
|
188
|
+
|
|
189
|
+
void notifySurfaceSize(double width, double height) {
|
|
190
|
+
std::function<void()> toCall;
|
|
191
|
+
{
|
|
192
|
+
const std::lock_guard<std::mutex> guard(lock());
|
|
193
|
+
if (surfaceSize().first == width && surfaceSize().second == height) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
surfaceSize() = {width, height};
|
|
197
|
+
toCall = dimensionsListener();
|
|
198
|
+
}
|
|
199
|
+
// Outside the lock, which emits into the runtime; see
|
|
200
|
+
// notifyWindowBoundsChanged below.
|
|
201
|
+
if (toCall) {
|
|
202
|
+
toCall();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
void lastKnownSurfaceSize(double *outWidth, double *outHeight) {
|
|
207
|
+
const std::lock_guard<std::mutex> guard(lock());
|
|
208
|
+
if (outWidth != nullptr) {
|
|
209
|
+
*outWidth = surfaceSize().first;
|
|
210
|
+
}
|
|
211
|
+
if (outHeight != nullptr) {
|
|
212
|
+
*outHeight = surfaceSize().second;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
void setDimensionsListener(std::function<void()> listener) {
|
|
217
|
+
const std::lock_guard<std::mutex> guard(lock());
|
|
218
|
+
dimensionsListener() = std::move(listener);
|
|
219
|
+
}
|
|
220
|
+
|
|
169
221
|
void notifyWindowBoundsChanged() {
|
|
170
222
|
// Asked on the UI thread, which is where this is called from, and stored
|
|
171
223
|
// where the JavaScript thread can read it.
|
|
172
224
|
const WindowBounds bounds = windowBounds();
|
|
173
225
|
|
|
174
226
|
std::function<void(const WindowBounds &)> toCall;
|
|
227
|
+
std::function<void()> toCallForDimensions;
|
|
175
228
|
{
|
|
176
229
|
const std::lock_guard<std::mutex> guard(lock());
|
|
177
230
|
cache() = bounds;
|
|
178
231
|
toCall = listener();
|
|
232
|
+
toCallForDimensions = dimensionsListener();
|
|
179
233
|
}
|
|
180
|
-
// Called outside the lock:
|
|
234
|
+
// Called outside the lock: they emit device events, and holding a lock across
|
|
181
235
|
// a call into the runtime is how a deadlock is built.
|
|
182
236
|
if (toCall) {
|
|
183
237
|
toCall(bounds);
|
|
184
238
|
}
|
|
239
|
+
if (toCallForDimensions) {
|
|
240
|
+
toCallForDimensions();
|
|
241
|
+
}
|
|
185
242
|
}
|
|
186
243
|
|
|
187
244
|
// --- Windows closing themselves ----------------------------------------------
|
|
@@ -70,6 +70,30 @@ void setWindowFullScreen(bool fullScreen);
|
|
|
70
70
|
// nothing removes it.
|
|
71
71
|
void setWindowBoundsListener(std::function<void(const WindowBounds &)> listener);
|
|
72
72
|
|
|
73
|
+
// How big the area React lays out in is, which is not the window's frame: a
|
|
74
|
+
// title bar belongs to the window and not to the app.
|
|
75
|
+
//
|
|
76
|
+
// Reported by each host wherever it starts or re-constrains its *main* surface,
|
|
77
|
+
// which is the one place that already knows this number -- and the number
|
|
78
|
+
// `Dimensions.get('window')` should answer with, since it is what an app can
|
|
79
|
+
// put something in. A second window and the error inspector have surfaces of
|
|
80
|
+
// their own and are deliberately not reported: `Dimensions` is singular, and
|
|
81
|
+
// the app's window is the one it means.
|
|
82
|
+
void notifySurfaceSize(double width, double height);
|
|
83
|
+
|
|
84
|
+
// The last size reported, or zeroes before the first surface starts. Readable
|
|
85
|
+
// from any thread, like the bounds above and for the same reason.
|
|
86
|
+
void lastKnownSurfaceSize(double *outWidth, double *outHeight);
|
|
87
|
+
|
|
88
|
+
// A second slot on the same signal, for `Dimensions`.
|
|
89
|
+
//
|
|
90
|
+
// Two rather than one because the two listeners have different lifetimes and
|
|
91
|
+
// neither owns the other: `useWindow()`'s module exists only in an app that
|
|
92
|
+
// imports this package, and React Native's `DeviceInfo` module exists in every
|
|
93
|
+
// app -- `Dimensions.js` requires it at import. A single slot would mean
|
|
94
|
+
// whichever was constructed second silently took the other's events.
|
|
95
|
+
void setDimensionsListener(std::function<void()> listener);
|
|
96
|
+
|
|
73
97
|
// The host's half of that: called from wherever it already learns the window
|
|
74
98
|
// moved or resized, on the UI thread. Also updates the cache below, so it is
|
|
75
99
|
// worth calling once when the window is first shown even if nothing is
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// `writingDirection`, as a name both hosts print.
|
|
2
|
+
//
|
|
3
|
+
// The prop decides which edge a paragraph starts from, and the two text engines
|
|
4
|
+
// take it in their own terms: a Pango context's base direction, an
|
|
5
|
+
// `NSParagraphStyle`'s `baseWritingDirection`. What is shared is only the word
|
|
6
|
+
// the tree dump reports, which exists so the two dumps compare line by line.
|
|
7
|
+
//
|
|
8
|
+
// `natural` is printed rather than left out. Asking for the Unicode algorithm's
|
|
9
|
+
// answer is not the same as saying nothing: a nested `<Text>` inherits the
|
|
10
|
+
// enclosing paragraph's direction when nothing is said, and the algorithm's
|
|
11
|
+
// answer when `natural` is.
|
|
12
|
+
|
|
13
|
+
#pragma once
|
|
14
|
+
|
|
15
|
+
#include <react/renderer/attributedstring/AttributedString.h>
|
|
16
|
+
|
|
17
|
+
#include <optional>
|
|
18
|
+
|
|
19
|
+
namespace basalt {
|
|
20
|
+
|
|
21
|
+
// The direction the paragraph asked for, from the first fragment that has one.
|
|
22
|
+
// React Native resolves a paragraph's attributes onto every fragment in it, so
|
|
23
|
+
// the first is the paragraph's; a nested `<Text>` with its own direction is a
|
|
24
|
+
// limit this shares with the text shadow, and backlog/text.md records it.
|
|
25
|
+
inline std::optional<facebook::react::WritingDirection> writingDirection(
|
|
26
|
+
const facebook::react::AttributedString &attributedString) {
|
|
27
|
+
for (const auto &fragment : attributedString.getFragments()) {
|
|
28
|
+
if (fragment.textAttributes.baseWritingDirection.has_value()) {
|
|
29
|
+
return fragment.textAttributes.baseWritingDirection;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return std::nullopt;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Its name, or nullptr when the app asked for nothing.
|
|
36
|
+
inline const char *writingDirectionName(
|
|
37
|
+
std::optional<facebook::react::WritingDirection> direction) {
|
|
38
|
+
if (!direction.has_value()) {
|
|
39
|
+
return nullptr;
|
|
40
|
+
}
|
|
41
|
+
switch (*direction) {
|
|
42
|
+
case facebook::react::WritingDirection::Natural:
|
|
43
|
+
return "natural";
|
|
44
|
+
case facebook::react::WritingDirection::LeftToRight:
|
|
45
|
+
return "ltr";
|
|
46
|
+
case facebook::react::WritingDirection::RightToLeft:
|
|
47
|
+
return "rtl";
|
|
48
|
+
}
|
|
49
|
+
return nullptr;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
} // namespace basalt
|
|
@@ -109,6 +109,13 @@ void showMenu(const MenuRequest &, MenuCallback onChosen) {
|
|
|
109
109
|
onChosen(-1);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
// And its roles. A platform that cannot show a menu performs nothing in one, so
|
|
113
|
+
// every role is unsupported here and `entriesFrom` drops them all -- which is
|
|
114
|
+
// the same answer this file gives everywhere: no, and in the right shape.
|
|
115
|
+
bool menuRoleSupported(const std::string &) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
void performMenuRole(const std::string &) {}
|
|
112
119
|
// Added when the gesture recognisers arrived: "run this on the UI thread", now
|
|
113
120
|
// and later. A platform with a run loop has both already; this one has neither
|
|
114
121
|
// and says so.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// A font file the machine does not have, built from one it does.
|
|
2
|
+
//
|
|
3
|
+
// Shared by both suites' font tests, because both have the same problem. The
|
|
4
|
+
// case `expo-font` exists for is a family the system has never heard of: an app
|
|
5
|
+
// ships Inter, loads it at runtime, and `fontFamily: 'Inter'` has to start
|
|
6
|
+
// working. A test that registers a font already in the system index proves the
|
|
7
|
+
// name mapping and nothing else, because every layer underneath already knew
|
|
8
|
+
// that family -- which is exactly how GTK's Pango-context reload and macOS's
|
|
9
|
+
// real registration path came to have no test.
|
|
10
|
+
//
|
|
11
|
+
// So this takes a real font file and renames the family *inside* it, which is
|
|
12
|
+
// the one thing that makes it new. The rename is done in place, one byte per
|
|
13
|
+
// occurrence, so nothing in the file moves: a name record's length and every
|
|
14
|
+
// offset after it stay valid, and no checksum has to be recomputed. The
|
|
15
|
+
// replacement letter is applied to the family's first character wherever the
|
|
16
|
+
// name table spells it, in ASCII and in the UTF-16BE that Windows-platform
|
|
17
|
+
// name records use.
|
|
18
|
+
//
|
|
19
|
+
// Shipping a fixture font would be the other way to do this, and it would mean
|
|
20
|
+
// a binary in the repository plus its licence, for a file that would still have
|
|
21
|
+
// to be renamed to be sure the machine lacks it.
|
|
22
|
+
|
|
23
|
+
#pragma once
|
|
24
|
+
|
|
25
|
+
#include <cstdint>
|
|
26
|
+
#include <cstdio>
|
|
27
|
+
#include <string>
|
|
28
|
+
#include <vector>
|
|
29
|
+
|
|
30
|
+
namespace basalt::test {
|
|
31
|
+
|
|
32
|
+
struct RenamedFont {
|
|
33
|
+
// Empty when the source could not be read or has no name table to patch, so
|
|
34
|
+
// a test can say so rather than assert on a file that was never written.
|
|
35
|
+
std::string path;
|
|
36
|
+
std::string family;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
namespace detail {
|
|
40
|
+
|
|
41
|
+
inline std::uint16_t beU16(const unsigned char *at) {
|
|
42
|
+
return static_cast<std::uint16_t>((at[0] << 8) | at[1]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
inline std::uint32_t beU32(const unsigned char *at) {
|
|
46
|
+
return (static_cast<std::uint32_t>(at[0]) << 24) | (static_cast<std::uint32_t>(at[1]) << 16) |
|
|
47
|
+
(static_cast<std::uint32_t>(at[2]) << 8) | static_cast<std::uint32_t>(at[3]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Replaces one byte in every occurrence of `needle` between `from` and `to`.
|
|
51
|
+
// `at` picks which byte of the match, which is 0 for ASCII and 1 for UTF-16BE,
|
|
52
|
+
// where the high byte of a Latin letter is zero.
|
|
53
|
+
inline int patchOccurrences(std::vector<unsigned char> &bytes,
|
|
54
|
+
std::size_t from,
|
|
55
|
+
std::size_t to,
|
|
56
|
+
const std::vector<unsigned char> &needle,
|
|
57
|
+
std::size_t at,
|
|
58
|
+
unsigned char replacement) {
|
|
59
|
+
if (needle.empty() || to > bytes.size() || from >= to) {
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
int patched = 0;
|
|
63
|
+
for (std::size_t i = from; i + needle.size() <= to; i++) {
|
|
64
|
+
bool same = true;
|
|
65
|
+
for (std::size_t j = 0; j < needle.size(); j++) {
|
|
66
|
+
if (bytes[i + j] != needle[j]) {
|
|
67
|
+
same = false;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (same) {
|
|
72
|
+
bytes[i + at] = replacement;
|
|
73
|
+
patched++;
|
|
74
|
+
i += needle.size() - 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return patched;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
} // namespace detail
|
|
81
|
+
|
|
82
|
+
// Writes `source` to `destination` with `family`'s first letter replaced by
|
|
83
|
+
// `replacement` wherever the name table spells it, and answers with the new
|
|
84
|
+
// family name. `family` is the name the file calls itself, which is what the
|
|
85
|
+
// platform's own registry reads back.
|
|
86
|
+
inline RenamedFont renameFontFile(const std::string &source,
|
|
87
|
+
const std::string &family,
|
|
88
|
+
const std::string &destination,
|
|
89
|
+
char replacement = 'Q') {
|
|
90
|
+
if (family.empty() || family[0] == replacement) {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
std::FILE *in = std::fopen(source.c_str(), "rb");
|
|
95
|
+
if (in == nullptr) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
std::vector<unsigned char> bytes;
|
|
99
|
+
unsigned char chunk[16384];
|
|
100
|
+
while (const std::size_t read = std::fread(chunk, 1, sizeof(chunk), in)) {
|
|
101
|
+
bytes.insert(bytes.end(), chunk, chunk + read);
|
|
102
|
+
}
|
|
103
|
+
std::fclose(in);
|
|
104
|
+
|
|
105
|
+
// A collection holds several fonts and starts with its own header; the table
|
|
106
|
+
// directory is not where this expects it, so say no rather than patch bytes
|
|
107
|
+
// at a guess.
|
|
108
|
+
if (bytes.size() < 12 || detail::beU32(bytes.data()) == 0x74746366 /* 'ttcf' */) {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const std::uint16_t tables = detail::beU16(bytes.data() + 4);
|
|
113
|
+
std::size_t nameAt = 0;
|
|
114
|
+
std::size_t nameLength = 0;
|
|
115
|
+
for (std::uint16_t i = 0; i < tables; i++) {
|
|
116
|
+
const std::size_t record = 12 + std::size_t{16} * i;
|
|
117
|
+
if (record + 16 > bytes.size()) {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
if (detail::beU32(bytes.data() + record) == 0x6E616D65 /* 'name' */) {
|
|
121
|
+
nameAt = detail::beU32(bytes.data() + record + 8);
|
|
122
|
+
nameLength = detail::beU32(bytes.data() + record + 12);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (nameAt == 0 || nameLength == 0 || nameAt + nameLength > bytes.size()) {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const std::vector<unsigned char> ascii(family.begin(), family.end());
|
|
131
|
+
std::vector<unsigned char> utf16;
|
|
132
|
+
utf16.reserve(family.size() * 2);
|
|
133
|
+
for (const char character : family) {
|
|
134
|
+
utf16.push_back(0);
|
|
135
|
+
utf16.push_back(static_cast<unsigned char>(character));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
int patched = detail::patchOccurrences(bytes, nameAt, nameAt + nameLength, ascii, 0,
|
|
139
|
+
static_cast<unsigned char>(replacement));
|
|
140
|
+
patched += detail::patchOccurrences(bytes, nameAt, nameAt + nameLength, utf16, 1,
|
|
141
|
+
static_cast<unsigned char>(replacement));
|
|
142
|
+
if (patched == 0) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
std::FILE *out = std::fopen(destination.c_str(), "wb");
|
|
147
|
+
if (out == nullptr) {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
const std::size_t written = std::fwrite(bytes.data(), 1, bytes.size(), out);
|
|
151
|
+
std::fclose(out);
|
|
152
|
+
if (written != bytes.size()) {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
std::string renamed = family;
|
|
157
|
+
renamed[0] = replacement;
|
|
158
|
+
return RenamedFont{destination, renamed};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
} // namespace basalt::test
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
#include "TestHarness.h"
|
|
8
8
|
|
|
9
|
+
#include "CrashHandler.h"
|
|
10
|
+
|
|
9
11
|
#include <algorithm>
|
|
10
12
|
#include <cctype>
|
|
13
|
+
#include <iostream>
|
|
11
14
|
#include <sstream>
|
|
12
15
|
|
|
13
16
|
namespace basalt::testing {
|
|
@@ -47,6 +50,21 @@ std::string lowered(std::string text) {
|
|
|
47
50
|
} // namespace
|
|
48
51
|
|
|
49
52
|
int runAllTests(int argc, char **argv) {
|
|
53
|
+
// Unbuffered, and this is not a style choice. `basalt_appkit_tests`
|
|
54
|
+
// segfaulted on CI's Mac and the log said only "Segmentation fault: 11": not
|
|
55
|
+
// one test name, because stdout to a pipe is block-buffered and the buffer
|
|
56
|
+
// dies with the process. On a developer's machine it is a terminal and
|
|
57
|
+
// line-buffered, so the same crash would have named the test.
|
|
58
|
+
//
|
|
59
|
+
// A unit suite that cannot say where it died is a suite that costs a CI round
|
|
60
|
+
// trip per guess, which is what this cost.
|
|
61
|
+
std::cout << std::unitbuf;
|
|
62
|
+
|
|
63
|
+
// And the handler from core/CrashHandler.h, for the half a flush cannot give:
|
|
64
|
+
// which frame faulted. The hosts install it at startup and the suites did
|
|
65
|
+
// not, so a crash in a *test* said less than a crash in an app.
|
|
66
|
+
basalt::installCrashHandler();
|
|
67
|
+
|
|
50
68
|
// Arguments are substrings of test names, and a test runs if it matches any
|
|
51
69
|
// of them. There is a lot of the day in the difference between running one
|
|
52
70
|
// test and running all of them, and this runner used to accept an argument
|