basalt-core 0.0.0-rc5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/basalt.d.ts.map +1 -1
- package/dist/cli/basalt.js +18 -1
- package/dist/cli/basalt.js.map +1 -1
- package/dist/cli/bundle-cli.js +0 -0
- package/dist/cli/desktop.d.ts.map +1 -1
- package/dist/cli/desktop.js +53 -4
- package/dist/cli/desktop.js.map +1 -1
- package/dist/cli/packageApp.d.ts.map +1 -1
- package/dist/cli/packageApp.js +7 -1
- package/dist/cli/packageApp.js.map +1 -1
- package/dist/metro-config.d.ts.map +1 -1
- package/dist/metro-config.js +53 -5
- package/dist/metro-config.js.map +1 -1
- package/dist/src/KeyHandler.d.ts +1 -1
- package/dist/src/KeyHandler.d.ts.map +1 -1
- package/dist/src/KeyHandler.js.map +1 -1
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/overrides/Alert.d.ts +1 -1
- package/dist/src/overrides/Alert.js +1 -1
- package/dist/src/overrides/BaseViewConfig.d.ts +10 -3
- package/dist/src/overrides/BaseViewConfig.d.ts.map +1 -1
- package/dist/src/overrides/BaseViewConfig.js +21 -3
- package/dist/src/overrides/BaseViewConfig.js.map +1 -1
- package/dist/src/overrides/Button.d.ts +81 -0
- package/dist/src/overrides/Button.d.ts.map +1 -0
- package/dist/src/overrides/Button.js +109 -0
- package/dist/src/overrides/Button.js.map +1 -0
- package/dist/src/overrides/ScrollView.d.ts +25 -2
- package/dist/src/overrides/ScrollView.d.ts.map +1 -1
- package/dist/src/overrides/ScrollView.js +64 -7
- package/dist/src/overrides/ScrollView.js.map +1 -1
- package/dist/src/overrides/Settings.d.ts +81 -0
- package/dist/src/overrides/Settings.d.ts.map +1 -0
- package/dist/src/overrides/Settings.js +120 -0
- package/dist/src/overrides/Settings.js.map +1 -0
- package/dist/src/overrides/processDecelerationRate.d.ts +20 -0
- package/dist/src/overrides/processDecelerationRate.d.ts.map +1 -0
- package/dist/src/overrides/processDecelerationRate.js +31 -0
- package/dist/src/overrides/processDecelerationRate.js.map +1 -0
- package/dist/src/useContextMenu.d.ts +57 -6
- package/dist/src/useContextMenu.d.ts.map +1 -1
- package/dist/src/useContextMenu.js +34 -1
- package/dist/src/useContextMenu.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/native/CMakeLists.txt +85 -1
- package/native/cmake/Skia.cmake +29 -6
- package/native/core/ActionSheet.cpp +75 -0
- package/native/core/ActionSheet.h +90 -0
- package/native/core/AppStateHost.cpp +76 -0
- package/native/core/AppStateHost.h +68 -0
- package/native/core/BackgroundLayers.h +282 -0
- package/native/core/BlendModes.h +67 -0
- package/native/core/CoreModules.cpp +380 -11
- package/native/core/CoreModules.h +140 -4
- package/native/core/CrashHandler.cpp +246 -0
- package/native/core/CrashHandler.h +65 -0
- package/native/core/CursorNames.h +110 -0
- package/native/core/DevMenu.cpp +6 -5
- package/native/core/DeviceMetrics.cpp +87 -0
- package/native/core/DeviceMetrics.h +80 -0
- package/native/core/Filters.h +336 -0
- package/native/core/FontFitting.h +183 -0
- package/native/core/FontScaling.h +153 -0
- package/native/core/FontVariants.h +148 -0
- package/native/core/Gradients.h +523 -0
- package/native/core/ImageAnimation.h +109 -0
- package/native/core/ImageBytes.cpp +28 -1
- package/native/core/ImageBytes.h +17 -1
- package/native/core/ImageProgress.h +85 -0
- package/native/core/LabelRegistry.h +189 -0
- package/native/core/LegacyShadow.h +90 -0
- package/native/core/LiveRegions.h +114 -0
- package/native/core/MenuModule.cpp +35 -4
- package/native/core/MountingWalk.h +125 -2
- package/native/core/PlatformServices.h +187 -3
- package/native/core/SafeAreaComponent.cpp +115 -0
- package/native/core/SafeAreaComponent.h +105 -0
- package/native/core/ScreensComponent.cpp +201 -0
- package/native/core/ScreensComponent.h +216 -0
- package/native/core/ScrollBounds.cpp +8 -0
- package/native/core/ScrollBounds.h +14 -0
- package/native/core/ScrollIndicator.h +67 -0
- package/native/core/ScrollMomentum.cpp +24 -0
- package/native/core/ScrollMomentum.h +14 -0
- package/native/core/ScrollSnap.cpp +121 -12
- package/native/core/ScrollSnap.h +45 -0
- package/native/core/ScrollVisiblePosition.cpp +68 -0
- package/native/core/ScrollVisiblePosition.h +101 -0
- package/native/core/SettingsStore.cpp +199 -0
- package/native/core/SettingsStore.h +127 -0
- package/native/core/ShadowShape.h +38 -0
- package/native/core/SkiaViewComponent.cpp +11 -0
- package/native/core/SkiaViewComponent.h +50 -0
- package/native/core/SystemLocale.cpp +103 -0
- package/native/core/SystemLocale.h +86 -0
- package/native/core/TestDialog.cpp +45 -6
- package/native/core/TestDialog.h +15 -0
- package/native/core/TextAlignments.h +240 -0
- package/native/core/TextChecking.h +133 -0
- package/native/core/TextColors.h +83 -0
- package/native/core/TextDecorations.h +101 -0
- package/native/core/TextDirection.cpp +242 -0
- package/native/core/TextDirection.h +88 -0
- package/native/core/TextHighlight.h +30 -0
- package/native/core/TextSelection.cpp +97 -0
- package/native/core/TextSelection.h +161 -0
- package/native/core/TextShadows.h +96 -0
- package/native/core/TextVerticalAlign.h +33 -0
- package/native/core/UIManagerAccess.cpp +36 -0
- package/native/core/UIManagerAccess.h +28 -0
- package/native/core/WindowBoundsCache.cpp +58 -1
- package/native/core/WindowControl.h +24 -0
- package/native/core/WritingDirections.h +52 -0
- package/native/core/portability_probe.cpp +7 -0
- package/native/tests/RenamedFontFile.h +161 -0
- package/native/tests/TestHarness.cpp +18 -0
- package/native/tests/test_action_sheet.cpp +157 -0
- package/native/tests/test_app_state.cpp +60 -0
- package/native/tests/test_background_layers.cpp +281 -0
- package/native/tests/test_blend_modes.cpp +79 -0
- package/native/tests/test_blobs.cpp +1 -1
- package/native/tests/test_device_metrics.cpp +126 -0
- package/native/tests/test_filters.cpp +271 -0
- package/native/tests/test_font_fitting.cpp +172 -0
- package/native/tests/test_font_scaling.cpp +127 -0
- package/native/tests/test_font_variants.cpp +84 -0
- package/native/tests/test_gradients.cpp +390 -0
- package/native/tests/test_image_animation.cpp +105 -0
- package/native/tests/test_image_progress.cpp +94 -0
- package/native/tests/test_label_registry.cpp +262 -0
- package/native/tests/test_legacy_shadow.cpp +165 -0
- package/native/tests/test_live_regions.cpp +108 -0
- package/native/tests/test_menu_entries.cpp +192 -0
- package/native/tests/test_momentum.cpp +45 -0
- package/native/tests/test_scroll_bounds.cpp +42 -0
- package/native/tests/test_scroll_indicator.cpp +61 -0
- package/native/tests/test_scroll_snap.cpp +129 -0
- package/native/tests/test_scroll_visible_position.cpp +121 -0
- package/native/tests/test_settings_store.cpp +257 -0
- package/native/tests/test_shadow_shape.cpp +47 -0
- package/native/tests/test_system_locale.cpp +36 -0
- package/native/tests/test_text_alignment.cpp +299 -0
- package/native/tests/test_text_checking.cpp +31 -0
- package/native/tests/test_text_colors.cpp +92 -0
- package/native/tests/test_text_decorations.cpp +100 -0
- package/native/tests/test_text_selection.cpp +168 -0
- package/native/tests/test_text_shadows.cpp +132 -0
- package/package.json +4 -2
|
@@ -34,10 +34,15 @@ bool boolProperty(Runtime &runtime, const Object &item, const char *name, bool f
|
|
|
34
34
|
return value.isBool() ? value.asBool() : fallback;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// A context menu's entries,
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
37
|
+
// A context menu's entries. Nested, and roles too: NSMenu, GMenu and an HMENU
|
|
38
|
+
// all nest, and the reason this was a flat list was never the platforms. It was
|
|
39
|
+
// that `showMenu` answers with an index and a tree has no single position -- see
|
|
40
|
+
// the note on `walkMenuEntries`, which is what settled it.
|
|
41
|
+
//
|
|
42
|
+
// A role an entry names and this desktop cannot perform is dropped here rather
|
|
43
|
+
// than shown, which is the application menu's policy for the same reason: an
|
|
44
|
+
// item that does nothing is worse than one that is not there. Linux drops most
|
|
45
|
+
// of them.
|
|
41
46
|
std::vector<MenuEntry> entriesFrom(Runtime &runtime, const Value &value) {
|
|
42
47
|
std::vector<MenuEntry> entries;
|
|
43
48
|
if (!value.isObject() || !value.asObject(runtime).isArray(runtime)) {
|
|
@@ -62,6 +67,32 @@ std::vector<MenuEntry> entriesFrom(Runtime &runtime, const Value &value) {
|
|
|
62
67
|
entry.label = stringProperty(runtime, item, "label");
|
|
63
68
|
entry.enabled = boolProperty(runtime, item, "enabled", true);
|
|
64
69
|
entry.shortcut = stringProperty(runtime, item, "shortcut");
|
|
70
|
+
entry.role = stringProperty(runtime, item, "role");
|
|
71
|
+
|
|
72
|
+
// `type` in Electron's spelling, because that is the vocabulary a desktop
|
|
73
|
+
// developer already has, and the roles above are Electron's for the same
|
|
74
|
+
// reason. `checked` on its own is read as a checkbox: it is the common case
|
|
75
|
+
// and making it need two words to work would be a trap.
|
|
76
|
+
const std::string type = stringProperty(runtime, item, "type");
|
|
77
|
+
const bool hasChecked = item.hasProperty(runtime, "checked");
|
|
78
|
+
if (type == "radio") {
|
|
79
|
+
entry.mark = MenuEntry::Mark::Radio;
|
|
80
|
+
} else if (type == "checkbox" || hasChecked) {
|
|
81
|
+
entry.mark = MenuEntry::Mark::Check;
|
|
82
|
+
}
|
|
83
|
+
if (entry.mark != MenuEntry::Mark::None) {
|
|
84
|
+
entry.checked = boolProperty(runtime, item, "checked", false);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const Value nested = item.getProperty(runtime, "submenu");
|
|
88
|
+
if (nested.isObject() && nested.asObject(runtime).isArray(runtime)) {
|
|
89
|
+
entry.submenu = entriesFrom(runtime, nested);
|
|
90
|
+
}
|
|
91
|
+
// Nothing is filtered here, deliberately. An entry naming a role this
|
|
92
|
+
// desktop cannot perform is not drawn -- `menuEntryShown` says so and each
|
|
93
|
+
// host skips it -- but it keeps its place in the vector, because the index
|
|
94
|
+
// that comes back is an index into the list JavaScript passed. Dropping it
|
|
95
|
+
// here would renumber everything after it onto the wrong handler.
|
|
65
96
|
entries.push_back(std::move(entry));
|
|
66
97
|
}
|
|
67
98
|
return entries;
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
|
|
53
53
|
#include <algorithm>
|
|
54
54
|
#include <cassert>
|
|
55
|
+
#include <atomic>
|
|
55
56
|
#include <cstdint>
|
|
56
57
|
#include <memory>
|
|
57
58
|
#include <string_view>
|
|
@@ -85,6 +86,13 @@ class MountingWalk {
|
|
|
85
86
|
// types, and because a view can sit detached between a Remove and its Delete
|
|
86
87
|
// while still needing to deliver a cancel.
|
|
87
88
|
facebook::react::EventEmitter::Shared eventEmitterForTag(Tag tag) const {
|
|
89
|
+
// Nothing, once an instance teardown has been announced: every emitter in
|
|
90
|
+
// the map points into a Scheduler that is going away, and handing one out
|
|
91
|
+
// is a call into freed memory. The map itself is emptied on this thread --
|
|
92
|
+
// see `invalidatePendingMounts`.
|
|
93
|
+
if (emittersInvalid_.load(std::memory_order_acquire)) {
|
|
94
|
+
return nullptr;
|
|
95
|
+
}
|
|
88
96
|
const auto it = eventEmitters_.find(tag);
|
|
89
97
|
return it == eventEmitters_.end() ? nullptr : it->second;
|
|
90
98
|
}
|
|
@@ -243,6 +251,11 @@ class MountingWalk {
|
|
|
243
251
|
// core/TestSettle.h.
|
|
244
252
|
noteMountApplied();
|
|
245
253
|
|
|
254
|
+
// Before anything is walked: an instance teardown may have announced itself
|
|
255
|
+
// from another thread while this transaction sat on the queue, and the
|
|
256
|
+
// emitters it left behind point into a Scheduler that is gone.
|
|
257
|
+
dropInvalidatedEmitters();
|
|
258
|
+
|
|
246
259
|
for (const auto &mutation : mutations) {
|
|
247
260
|
switch (mutation.type) {
|
|
248
261
|
case facebook::react::ShadowViewMutation::Create:
|
|
@@ -273,12 +286,92 @@ class MountingWalk {
|
|
|
273
286
|
resolveRefreshControls();
|
|
274
287
|
}
|
|
275
288
|
|
|
289
|
+
// --- A mount that outlived the instance that made it ----------------------
|
|
290
|
+
//
|
|
291
|
+
// `executeMount` is called on the JS thread and every host queues the
|
|
292
|
+
// transaction to its UI thread, so one can still be waiting there when the
|
|
293
|
+
// React instance that produced it is torn down. A reload does exactly that,
|
|
294
|
+
// and `ReactHost::reloadReactInstance` does it on a detached thread: it stops
|
|
295
|
+
// the surfaces, destroys the Scheduler and the SurfaceManager, and builds new
|
|
296
|
+
// ones, while the mounting manager survives. Applying a queued transaction
|
|
297
|
+
// after that walks mutations whose event emitters point into an instance that
|
|
298
|
+
// is gone, and the host dies in its own UI thread with nothing to say.
|
|
299
|
+
//
|
|
300
|
+
// So a pending mount carries these two and drops itself when either says it is
|
|
301
|
+
// stale. `weak_ptr` rather than testing `use_count()`: several mounts can be
|
|
302
|
+
// queued at once, and each one's own copy would otherwise look like the
|
|
303
|
+
// manager still holding its.
|
|
304
|
+
std::weak_ptr<bool> mountGuard() const {
|
|
305
|
+
return alive_;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
std::uint64_t mountEpoch() const {
|
|
309
|
+
return mountEpoch_.load(std::memory_order_relaxed);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Called by a host when React Native hands it a null scheduler task executor,
|
|
313
|
+
// which is what `destroyReactInstance` does and the only notice a mounting
|
|
314
|
+
// manager gets that the instance is going away.
|
|
315
|
+
void invalidatePendingMounts() {
|
|
316
|
+
mountEpoch_.fetch_add(1, std::memory_order_relaxed);
|
|
317
|
+
// The emitters are the part that points into the dead instance. The views
|
|
318
|
+
// stay: the surfaces restart with the same ids, the host owns their roots,
|
|
319
|
+
// and the next transaction replaces what is under them. An event arriving in
|
|
320
|
+
// the gap now finds no emitter and does nothing, which is the right answer
|
|
321
|
+
// and was previously a call into freed memory.
|
|
322
|
+
//
|
|
323
|
+
// **A flag rather than the clear itself, and that is the whole of a crash
|
|
324
|
+
// this project chased twice.** This function runs on whatever thread
|
|
325
|
+
// `destroyReactInstance` is on, which is not the main one, while a mount
|
|
326
|
+
// queued earlier may be draining on the main thread -- and `eventEmitters_`
|
|
327
|
+
// is a `std::unordered_map`. Clearing it from under `destroy()`'s own
|
|
328
|
+
// `erase` is a plain data race, and what it produces is a wild pointer
|
|
329
|
+
// inside the hash table's `find`: `host exited -11` on CI's Mac, twice,
|
|
330
|
+
// with `MountingWalk::destroy` and an `unordered_map::erase` in the stack.
|
|
331
|
+
// backlog/testing.md carries both occurrences.
|
|
332
|
+
//
|
|
333
|
+
// So the flag goes up atomically, nothing hands an emitter out from this
|
|
334
|
+
// moment on -- which is what the clear was for -- and the map is emptied by
|
|
335
|
+
// the main thread the next time it walks a transaction or releases its
|
|
336
|
+
// views.
|
|
337
|
+
emittersInvalid_.store(true, std::memory_order_release);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// How many emitters are still remembered, whatever the flag above says.
|
|
341
|
+
//
|
|
342
|
+
// Only the suite asks, and it asks because the thing worth asserting about
|
|
343
|
+
// the teardown is *which thread* empties the map: an announcement that
|
|
344
|
+
// cleared it itself would be the data race this project chased twice, and
|
|
345
|
+
// `eventEmitterForTag` cannot tell the two apart -- it answers nothing either
|
|
346
|
+
// way. See `a_teardown_leaves_the_clearing_to_the_main_thread`.
|
|
347
|
+
std::size_t rememberedEmitterCount() const {
|
|
348
|
+
return eventEmitters_.size();
|
|
349
|
+
}
|
|
350
|
+
|
|
276
351
|
protected:
|
|
277
352
|
// Not virtual and not public: this is a mixin, never a base pointer.
|
|
278
353
|
~MountingWalk() = default;
|
|
279
354
|
|
|
355
|
+
// Empties the emitter map if a teardown asked for it, on the thread that owns
|
|
356
|
+
// it. The one place the map is cleared, and the reason it is not cleared where
|
|
357
|
+
// the asking happens: see `invalidatePendingMounts`.
|
|
358
|
+
void dropInvalidatedEmitters() {
|
|
359
|
+
if (emittersInvalid_.exchange(false, std::memory_order_acq_rel)) {
|
|
360
|
+
eventEmitters_.clear();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
280
364
|
// Platforms call this from their own destructor. See the note at the top.
|
|
281
365
|
void releaseAllViews() {
|
|
366
|
+
dropInvalidatedEmitters();
|
|
367
|
+
// Same ordering as a Delete, and for the same reason: a side table holding a
|
|
368
|
+
// view has to be told before the view goes, not after. This path is a host's
|
|
369
|
+
// own destructor, so getting it wrong is a crash on the way out -- the kind
|
|
370
|
+
// that looks like a flaky test rather than a bug.
|
|
371
|
+
for (auto &[tag, view] : registry_) {
|
|
372
|
+
(void)view;
|
|
373
|
+
platform().forgetTag(tag);
|
|
374
|
+
}
|
|
282
375
|
for (auto &[tag, view] : registry_) {
|
|
283
376
|
(void)tag;
|
|
284
377
|
platform().destroyView(view);
|
|
@@ -370,9 +463,17 @@ class MountingWalk {
|
|
|
370
463
|
// in a later transaction.
|
|
371
464
|
void create(const ShadowView &shadowView) {
|
|
372
465
|
ViewRef view = platform().createView(shadowView);
|
|
466
|
+
// The emitter before the props, which is not the obvious order and is the
|
|
467
|
+
// one an apply needs: `updateView` is where a platform acts on what it was
|
|
468
|
+
// given, and acting on some props means sending an event at once.
|
|
469
|
+
// `<Image onLoadStart>` is the case that found this -- it fires when a load
|
|
470
|
+
// begins, a load begins on the mount that first carries the source, and
|
|
471
|
+
// `eventEmitterForTag` answered null for every one of them because the map
|
|
472
|
+
// was filled two lines further down. The event went nowhere, silently, on
|
|
473
|
+
// all three hosts.
|
|
474
|
+
rememberEventEmitter(shadowView);
|
|
373
475
|
platform().updateView(view, shadowView);
|
|
374
476
|
registry_[shadowView.tag] = view;
|
|
375
|
-
rememberEventEmitter(shadowView);
|
|
376
477
|
}
|
|
377
478
|
|
|
378
479
|
void destroy(Tag tag) {
|
|
@@ -381,6 +482,14 @@ class MountingWalk {
|
|
|
381
482
|
LOG(WARNING) << "Delete for unknown tag " << tag;
|
|
382
483
|
return;
|
|
383
484
|
}
|
|
485
|
+
// The platform lets go of the tag *before* the view is released, which is
|
|
486
|
+
// the ordering every per-tag side table needs: a scroll controller to
|
|
487
|
+
// detach, a text peer to drop, a canvas to unregister. Each of those takes
|
|
488
|
+
// the view it remembers, and after `destroyView` that pointer can be freed
|
|
489
|
+
// memory -- GTK's managers were checking `RN_IS_VIEW` to find out, which
|
|
490
|
+
// cannot work and crashed once it was asked on a poisoned object. See
|
|
491
|
+
// GtkFocus.cpp's destructor for the one that bit.
|
|
492
|
+
platform().forgetTag(tag);
|
|
384
493
|
platform().destroyView(it->second);
|
|
385
494
|
registry_.erase(it);
|
|
386
495
|
eventEmitters_.erase(tag);
|
|
@@ -391,7 +500,6 @@ class MountingWalk {
|
|
|
391
500
|
pullToRefresh_.forget(tag);
|
|
392
501
|
modalViews_.erase(tag);
|
|
393
502
|
modalStack_.erase(std::remove(modalStack_.begin(), modalStack_.end(), tag), modalStack_.end());
|
|
394
|
-
platform().forgetTag(tag);
|
|
395
503
|
}
|
|
396
504
|
|
|
397
505
|
void insert(const facebook::react::ShadowViewMutation &mutation) {
|
|
@@ -516,6 +624,21 @@ class MountingWalk {
|
|
|
516
624
|
std::uint64_t overlayGeneration_{0};
|
|
517
625
|
std::shared_ptr<bool> alive_{std::make_shared<bool>(true)};
|
|
518
626
|
|
|
627
|
+
// Set when the React instance is torn down and cleared by the main thread;
|
|
628
|
+
// see invalidatePendingMounts and dropInvalidatedEmitters.
|
|
629
|
+
std::atomic<bool> emittersInvalid_{false};
|
|
630
|
+
|
|
631
|
+
// Bumped when the React instance is torn down; see invalidatePendingMounts.
|
|
632
|
+
//
|
|
633
|
+
// Atomic because the two ends are different threads: `destroyReactInstance`
|
|
634
|
+
// runs on a detached thread and bumps this, while `applyPendingMount` reads it
|
|
635
|
+
// on the main thread to decide whether to drop itself. A plain integer there is
|
|
636
|
+
// a data race on the guard that exists to prevent a use-after-free, which is
|
|
637
|
+
// the wrong place to have one. Relaxed ordering is enough: the only question
|
|
638
|
+
// asked of it is whether the value still equals the one a mount was queued
|
|
639
|
+
// with, and a mount that reads a stale epoch is dropped by the next one.
|
|
640
|
+
std::atomic<std::uint64_t> mountEpoch_{0};
|
|
641
|
+
|
|
519
642
|
// The main thread, recorded at construction. `executeMount` arrives on the JS
|
|
520
643
|
// thread and marshals here; `applyMutations` asserts it got there.
|
|
521
644
|
std::thread::id mainThreadId_;
|
|
@@ -11,15 +11,31 @@
|
|
|
11
11
|
|
|
12
12
|
#pragma once
|
|
13
13
|
|
|
14
|
+
#include <cstddef>
|
|
14
15
|
#include <functional>
|
|
15
16
|
#include <string>
|
|
17
|
+
#include <utility>
|
|
16
18
|
#include <vector>
|
|
17
19
|
|
|
18
20
|
namespace basalt {
|
|
19
21
|
|
|
20
22
|
// --- Clipboard ---------------------------------------------------------------
|
|
21
23
|
|
|
22
|
-
// The clipboard's text, or empty if it holds none.
|
|
24
|
+
// The clipboard's text, or empty if it holds none.
|
|
25
|
+
//
|
|
26
|
+
// **Called from the JavaScript thread**, by DesktopClipboardModule, which is
|
|
27
|
+
// where `Clipboard.getString()` and `setString()` arrive. This comment used to
|
|
28
|
+
// say "on the main thread", which was what the seam wanted rather than what it
|
|
29
|
+
// got: the GTK implementation believed it and called GDK from whichever thread
|
|
30
|
+
// asked, which deadlocked a CI run on 2026-10-08. See
|
|
31
|
+
// PlatformServicesGtk.cpp's clipboard section and backlog/testing.md.
|
|
32
|
+
//
|
|
33
|
+
// So each implementation answers for its own toolkit's threading rule. GTK hands
|
|
34
|
+
// the write to its main thread; Windows posts through a message-only window for
|
|
35
|
+
// other work and its clipboard calls are per-thread by design; macOS uses
|
|
36
|
+
// NSPasteboard, which is documented as not thread-safe and makes no blocking
|
|
37
|
+
// round trip against the main thread. backlog/host-wiring.md carries what is
|
|
38
|
+
// left.
|
|
23
39
|
std::string clipboardText();
|
|
24
40
|
void setClipboardText(const std::string &text);
|
|
25
41
|
|
|
@@ -105,6 +121,17 @@ void showFileDialog(const FileDialogRequest &request, FileDialogCallback onDone)
|
|
|
105
121
|
|
|
106
122
|
// --- Menus --------------------------------------------------------------------
|
|
107
123
|
|
|
124
|
+
// Whether this desktop can perform `role` in a popup menu.
|
|
125
|
+
//
|
|
126
|
+
// False for every role a platform has no implementation for: Linux and Windows
|
|
127
|
+
// both lack `about`, neither having a platform about panel to ask for. Declared
|
|
128
|
+
// here rather than below because MenuEntry asks it.
|
|
129
|
+
bool menuRoleSupported(const std::string &role);
|
|
130
|
+
|
|
131
|
+
// Performs `role` now, on whatever currently has focus. Called on the UI thread,
|
|
132
|
+
// immediately before the chosen index is reported.
|
|
133
|
+
void performMenuRole(const std::string &role);
|
|
134
|
+
|
|
108
135
|
// One entry in a popup menu. A separator is an item with an empty label, which
|
|
109
136
|
// is how GMenu, NSMenu and an HMENU each spell it too.
|
|
110
137
|
struct MenuEntry {
|
|
@@ -117,12 +144,58 @@ struct MenuEntry {
|
|
|
117
144
|
// inside it are answered in different places on all three desktops.
|
|
118
145
|
std::string shortcut;
|
|
119
146
|
|
|
147
|
+
// One of kMenuRoles in MenuModel.h, or empty for an item the app implements
|
|
148
|
+
// itself. A role is performed by the platform -- the Copy that actually
|
|
149
|
+
// copies -- and still reports its index, so a caller's `onSelect` runs too.
|
|
150
|
+
//
|
|
151
|
+
// A role this platform cannot perform is dropped rather than shown, the same
|
|
152
|
+
// policy the menu bar has: an item that does nothing is worse than one that is
|
|
153
|
+
// not there. `menuRoleSupported` says which, and the three do not agree.
|
|
154
|
+
std::string role;
|
|
155
|
+
|
|
156
|
+
// Nested entries. A parent is not choosable itself: it opens its children.
|
|
157
|
+
//
|
|
158
|
+
// Both a submenu and a role on one entry is a caller's mistake; the submenu
|
|
159
|
+
// wins, because that is the one with something under it to lose.
|
|
160
|
+
std::vector<MenuEntry> submenu;
|
|
161
|
+
|
|
162
|
+
// Whether the entry carries a mark, and which kind.
|
|
163
|
+
//
|
|
164
|
+
// Two fields rather than one `checked` bool, and GTK is the reason. A GMenu
|
|
165
|
+
// item has no checked attribute at all: the mark comes from the shape of the
|
|
166
|
+
// GAction behind it, and GtkPopoverMenu picks a tick or a circle from whether
|
|
167
|
+
// that action is boolean-stateful or string-stateful with a target. So a bool
|
|
168
|
+
// cannot say which to draw, and the kind has to be its own field.
|
|
169
|
+
//
|
|
170
|
+
// `Radio` is a drawing hint rather than a capability. Windows draws a bullet
|
|
171
|
+
// and GTK a circle; NSMenu has no radio item, and the mark macOS puts beside a
|
|
172
|
+
// selected member of an exclusive group is a tick, which is what Apple's own
|
|
173
|
+
// guidance prescribes. All three can mark a chosen item, so neither kind is
|
|
174
|
+
// refused anywhere.
|
|
175
|
+
//
|
|
176
|
+
// Nothing tri-state: NSControlStateValueMixed has no counterpart on the other
|
|
177
|
+
// two, so offering it would be offering something only one desktop has.
|
|
178
|
+
enum class Mark { None, Check, Radio };
|
|
179
|
+
Mark mark{Mark::None};
|
|
180
|
+
bool checked{false};
|
|
181
|
+
|
|
120
182
|
static MenuEntry separator() {
|
|
121
|
-
|
|
183
|
+
// Assigned rather than a designated initializer. One that names some fields
|
|
184
|
+
// and not others is a -Wmissing-field-initializers error under -Werror, which
|
|
185
|
+
// the GTK build turns on and the AppKit one does not -- so adding `role` and
|
|
186
|
+
// `submenu` compiled on a Mac and failed all three Linux shards. Written this
|
|
187
|
+
// way, the next field costs nothing.
|
|
188
|
+
MenuEntry entry;
|
|
189
|
+
entry.enabled = false;
|
|
190
|
+
return entry;
|
|
122
191
|
}
|
|
123
192
|
|
|
124
193
|
bool isSeparator() const {
|
|
125
|
-
return label.empty();
|
|
194
|
+
return label.empty() && submenu.empty();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
bool isParent() const {
|
|
198
|
+
return !submenu.empty();
|
|
126
199
|
}
|
|
127
200
|
};
|
|
128
201
|
|
|
@@ -134,6 +207,117 @@ struct MenuRequest {
|
|
|
134
207
|
double y{-1.0};
|
|
135
208
|
};
|
|
136
209
|
|
|
210
|
+
// --- Indexing a menu that is now a tree ---------------------------------------
|
|
211
|
+
//
|
|
212
|
+
// `showMenu` answers with an index, and did so when a popup was a flat list. The
|
|
213
|
+
// index is an entry's position in a *pre-order* walk of the whole menu, counting
|
|
214
|
+
// separators and submenu parents -- so for a flat list it is still the position
|
|
215
|
+
// in the vector, and every caller that predates nesting means what it always
|
|
216
|
+
// meant.
|
|
217
|
+
//
|
|
218
|
+
// Hosts use `walkMenuEntries` to tag their items and `menuEntryAt` to find what
|
|
219
|
+
// an index named, which is how a role is performed and how a parent is told from
|
|
220
|
+
// a leaf.
|
|
221
|
+
namespace detail {
|
|
222
|
+
|
|
223
|
+
template <typename Visitor>
|
|
224
|
+
int walkMenuEntriesFrom(const std::vector<MenuEntry> &entries, int next, Visitor &visit) {
|
|
225
|
+
for (const MenuEntry &entry : entries) {
|
|
226
|
+
const int index = next++;
|
|
227
|
+
visit(entry, index);
|
|
228
|
+
if (entry.isParent()) {
|
|
229
|
+
next = walkMenuEntriesFrom(entry.submenu, next, visit);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return next;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
} // namespace detail
|
|
236
|
+
|
|
237
|
+
template <typename Visitor>
|
|
238
|
+
void walkMenuEntries(const std::vector<MenuEntry> &entries, Visitor &&visit) {
|
|
239
|
+
detail::walkMenuEntriesFrom(entries, 0, visit);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// The run of consecutive radio entries `index` belongs to, as a half-open range
|
|
243
|
+
// of pre-order indexes, or {index, index} when it is not a radio entry.
|
|
244
|
+
//
|
|
245
|
+
// A group is a maximal run of adjacent radio siblings, ended by a separator, by a
|
|
246
|
+
// non-radio entry, or by the end of the level. That is a positional definition
|
|
247
|
+
// rather than an identifier on each item, and it fits all three: Win32's
|
|
248
|
+
// CheckMenuRadioItem wants a contiguous range, GTK wants one action per run, and
|
|
249
|
+
// AppKit wants nothing. It also cannot straddle a GMenu section, since the GTK
|
|
250
|
+
// builder breaks into a new section at every separator.
|
|
251
|
+
//
|
|
252
|
+
// The cost is that two adjacent groups with no separator between them read as
|
|
253
|
+
// one. A separator is what every desktop draws between two groups anyway.
|
|
254
|
+
inline std::pair<int, int> menuRadioRun(const std::vector<MenuEntry> &siblings,
|
|
255
|
+
int firstIndexOfLevel,
|
|
256
|
+
std::size_t at) {
|
|
257
|
+
const int index = firstIndexOfLevel + static_cast<int>(at);
|
|
258
|
+
if (at >= siblings.size() || siblings[at].mark != MenuEntry::Mark::Radio) {
|
|
259
|
+
return {index, index};
|
|
260
|
+
}
|
|
261
|
+
std::size_t begin = at;
|
|
262
|
+
while (begin > 0 && siblings[begin - 1].mark == MenuEntry::Mark::Radio) {
|
|
263
|
+
begin--;
|
|
264
|
+
}
|
|
265
|
+
std::size_t end = at + 1;
|
|
266
|
+
while (end < siblings.size() && siblings[end].mark == MenuEntry::Mark::Radio) {
|
|
267
|
+
end++;
|
|
268
|
+
}
|
|
269
|
+
return {firstIndexOfLevel + static_cast<int>(begin),
|
|
270
|
+
firstIndexOfLevel + static_cast<int>(end)};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// How many entries a menu has, counting every level. The number of valid
|
|
274
|
+
// indexes, which is not `entries.size()` once anything nests.
|
|
275
|
+
inline int menuEntryCount(const std::vector<MenuEntry> &entries) {
|
|
276
|
+
int count = 0;
|
|
277
|
+
walkMenuEntries(entries, [&](const MenuEntry &, int) { ++count; });
|
|
278
|
+
return count;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
inline const MenuEntry *menuEntryAt(const std::vector<MenuEntry> &entries, int index) {
|
|
282
|
+
const MenuEntry *found = nullptr;
|
|
283
|
+
walkMenuEntries(entries, [&](const MenuEntry &entry, int at) {
|
|
284
|
+
if (at == index) {
|
|
285
|
+
found = &entry;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
return found;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Whether an entry is drawn at all.
|
|
292
|
+
//
|
|
293
|
+
// False for a role this desktop cannot perform, and for a parent with nothing
|
|
294
|
+
// under it that would be drawn. A dropped entry is still *counted*, which is the
|
|
295
|
+
// part that matters: JavaScript numbers the list it passed, so an entry
|
|
296
|
+
// disappearing from the count would shift every index after it onto the wrong
|
|
297
|
+
// handler. See walkMenuEntries.
|
|
298
|
+
inline bool menuEntryShown(const MenuEntry &entry);
|
|
299
|
+
|
|
300
|
+
inline bool anyMenuEntryShown(const std::vector<MenuEntry> &entries) {
|
|
301
|
+
for (const MenuEntry &entry : entries) {
|
|
302
|
+
if (menuEntryShown(entry)) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
inline bool menuEntryShown(const MenuEntry &entry) {
|
|
310
|
+
if (!entry.role.empty() && !menuRoleSupported(entry.role)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if (entry.isParent()) {
|
|
314
|
+
// An empty submenu is a dead end rather than an item, which is what a parent
|
|
315
|
+
// becomes when every child named a role this desktop does not have.
|
|
316
|
+
return anyMenuEntryShown(entry.submenu);
|
|
317
|
+
}
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
137
321
|
// Shows a popup menu and calls `onChosen` with the index of the entry picked,
|
|
138
322
|
// or -1 when it was dismissed. Indexes count separators, so they line up with
|
|
139
323
|
// the vector that was passed in.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#include "SafeAreaComponent.h"
|
|
2
|
+
|
|
3
|
+
#include <jsi/jsi.h>
|
|
4
|
+
|
|
5
|
+
namespace facebook::react {
|
|
6
|
+
|
|
7
|
+
extern const char SafeAreaProviderComponentName[] = "RNCSafeAreaProvider";
|
|
8
|
+
|
|
9
|
+
void SafeAreaProviderEventEmitter::onInsetsChange(const Rect &frame,
|
|
10
|
+
const EdgeInsets &insets) const {
|
|
11
|
+
dispatchEvent("insetsChange", [frame, insets](jsi::Runtime &runtime) {
|
|
12
|
+
auto frameOut = jsi::Object(runtime);
|
|
13
|
+
frameOut.setProperty(runtime, "x", frame.origin.x);
|
|
14
|
+
frameOut.setProperty(runtime, "y", frame.origin.y);
|
|
15
|
+
frameOut.setProperty(runtime, "width", frame.size.width);
|
|
16
|
+
frameOut.setProperty(runtime, "height", frame.size.height);
|
|
17
|
+
|
|
18
|
+
auto insetsOut = jsi::Object(runtime);
|
|
19
|
+
insetsOut.setProperty(runtime, "top", insets.top);
|
|
20
|
+
insetsOut.setProperty(runtime, "right", insets.right);
|
|
21
|
+
insetsOut.setProperty(runtime, "bottom", insets.bottom);
|
|
22
|
+
insetsOut.setProperty(runtime, "left", insets.left);
|
|
23
|
+
|
|
24
|
+
auto payload = jsi::Object(runtime);
|
|
25
|
+
payload.setProperty(runtime, "frame", std::move(frameOut));
|
|
26
|
+
payload.setProperty(runtime, "insets", std::move(insetsOut));
|
|
27
|
+
return payload;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Emitted on every layout that produces a real frame, rather than only on a
|
|
32
|
+
// change.
|
|
33
|
+
//
|
|
34
|
+
// A shadow node is cloned per commit, so there is nowhere here to remember what
|
|
35
|
+
// was sent last without carrying Fabric State for it, and State for a value the
|
|
36
|
+
// platform already knows would be machinery for its own sake. The deduplication
|
|
37
|
+
// that matters is already in the library: `setInsets` and `setFrame` both
|
|
38
|
+
// compare before they store, so an identical event is a React state write that
|
|
39
|
+
// bails out without re-rendering, and the loop this would otherwise be settles
|
|
40
|
+
// after one pass.
|
|
41
|
+
//
|
|
42
|
+
// The zero-size guard is not an optimisation. The first layout of a surface can
|
|
43
|
+
// produce an empty frame, and emitting that would unblock the provider's
|
|
44
|
+
// children against a 0x0 frame, which is worse than making them wait one more
|
|
45
|
+
// pass for the real one.
|
|
46
|
+
void SafeAreaProviderShadowNode::layout(LayoutContext layoutContext) {
|
|
47
|
+
ConcreteViewShadowNode::layout(layoutContext);
|
|
48
|
+
|
|
49
|
+
const auto frame = getLayoutMetrics().frame;
|
|
50
|
+
if (frame.size.width <= 0 || frame.size.height <= 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// Zero on every desktop: a window has no notch and no home indicator. See the
|
|
54
|
+
// header for what would change that.
|
|
55
|
+
getConcreteEventEmitter().onInsetsChange(frame, EdgeInsets{0, 0, 0, 0});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
} // namespace facebook::react
|
|
59
|
+
|
|
60
|
+
namespace basalt {
|
|
61
|
+
namespace {
|
|
62
|
+
|
|
63
|
+
// Written once by the host thread before JavaScript runs and read from the JS
|
|
64
|
+
// thread afterwards, which is the ordering every host already has for its
|
|
65
|
+
// window. Plain doubles rather than an atomic pair: a torn read here would
|
|
66
|
+
// produce a slightly wrong first frame that the first `onInsetsChange`
|
|
67
|
+
// immediately replaces, and a mutex on a value read once is not worth it.
|
|
68
|
+
double gWindowWidth = 0;
|
|
69
|
+
double gWindowHeight = 0;
|
|
70
|
+
|
|
71
|
+
} // namespace
|
|
72
|
+
|
|
73
|
+
void setInitialWindowFrame(double width, double height) {
|
|
74
|
+
gWindowWidth = width;
|
|
75
|
+
gWindowHeight = height;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
DesktopSafeAreaModule::DesktopSafeAreaModule(
|
|
79
|
+
std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
|
|
80
|
+
: TurboModule(kModuleName, std::move(jsInvoker)) {
|
|
81
|
+
methodMap_["getConstants"] = MethodMetadata{0, getConstants};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
facebook::jsi::Value DesktopSafeAreaModule::getConstants(
|
|
85
|
+
facebook::jsi::Runtime &runtime,
|
|
86
|
+
facebook::react::TurboModule & /*module*/,
|
|
87
|
+
const facebook::jsi::Value * /*args*/,
|
|
88
|
+
size_t /*count*/) {
|
|
89
|
+
auto constants = facebook::jsi::Object(runtime);
|
|
90
|
+
if (gWindowWidth <= 0 || gWindowHeight <= 0) {
|
|
91
|
+
// Absent rather than zero. See setInitialWindowFrame.
|
|
92
|
+
return constants;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
auto insets = facebook::jsi::Object(runtime);
|
|
96
|
+
insets.setProperty(runtime, "top", 0);
|
|
97
|
+
insets.setProperty(runtime, "right", 0);
|
|
98
|
+
insets.setProperty(runtime, "bottom", 0);
|
|
99
|
+
insets.setProperty(runtime, "left", 0);
|
|
100
|
+
|
|
101
|
+
auto frame = facebook::jsi::Object(runtime);
|
|
102
|
+
frame.setProperty(runtime, "x", 0);
|
|
103
|
+
frame.setProperty(runtime, "y", 0);
|
|
104
|
+
frame.setProperty(runtime, "width", gWindowWidth);
|
|
105
|
+
frame.setProperty(runtime, "height", gWindowHeight);
|
|
106
|
+
|
|
107
|
+
auto metrics = facebook::jsi::Object(runtime);
|
|
108
|
+
metrics.setProperty(runtime, "insets", std::move(insets));
|
|
109
|
+
metrics.setProperty(runtime, "frame", std::move(frame));
|
|
110
|
+
|
|
111
|
+
constants.setProperty(runtime, "initialWindowMetrics", std::move(metrics));
|
|
112
|
+
return constants;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
} // namespace basalt
|