basalt-win32 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/native/CMakeLists.txt +56 -9
  2. package/native/tests/test_win32_accessibility.cpp +77 -0
  3. package/native/tests/test_win32_focus.cpp +26 -0
  4. package/native/tests/test_win32_input.cpp +82 -0
  5. package/native/tests/test_win32_peer_clip.cpp +266 -0
  6. package/native/tests/test_win32_run_loop.cpp +122 -0
  7. package/native/tests/test_win32_textinput.cpp +199 -0
  8. package/native/tests/test_win32_ui_timer.cpp +195 -0
  9. package/native/win32/PlatformServicesWin32.cpp +16 -0
  10. package/native/win32/RnWin32Accessible.cpp +27 -0
  11. package/native/win32/RnWin32Accessible.h +5 -0
  12. package/native/win32/RnWin32View.h +18 -0
  13. package/native/win32/Win32AnimationChoreographer.cpp +15 -32
  14. package/native/win32/Win32AnimationChoreographer.h +9 -6
  15. package/native/win32/Win32AppWindow.h +36 -0
  16. package/native/win32/Win32Cursors.cpp +13 -17
  17. package/native/win32/Win32DropTarget.cpp +4 -1
  18. package/native/win32/Win32MenuBar.cpp +3 -1
  19. package/native/win32/Win32MountingManager.cpp +82 -7
  20. package/native/win32/Win32MountingManager.h +5 -0
  21. package/native/win32/Win32PeerClip.cpp +153 -0
  22. package/native/win32/Win32PeerClip.h +92 -0
  23. package/native/win32/Win32RunLoopObserver.cpp +39 -9
  24. package/native/win32/Win32RunLoopObserver.h +41 -2
  25. package/native/win32/Win32ScrollView.cpp +13 -32
  26. package/native/win32/Win32ScrollView.h +5 -6
  27. package/native/win32/Win32SkiaPeer.h +26 -4
  28. package/native/win32/Win32TextInput.cpp +139 -48
  29. package/native/win32/Win32TextInput.h +28 -8
  30. package/native/win32/Win32UiThread.h +11 -0
  31. package/native/win32/Win32UiTimer.cpp +139 -0
  32. package/native/win32/Win32UiTimer.h +65 -0
  33. package/native/win32/Win32WindowControl.cpp +38 -15
  34. package/native/win32/Win32WindowModule.cpp +8 -13
  35. package/native/win32/main_win32.cpp +123 -96
  36. package/package.json +2 -2
@@ -55,8 +55,17 @@
55
55
  #ifdef BASALT_HAS_SKIA
56
56
  #include "Win32SkiaModule.h"
57
57
  #endif
58
+ #if BASALT_HAS_SKIA_GRAPHITE
59
+ // The 3.x module, from basalt-skia. Registered here rather than through
60
+ // BASALT_PACKAGE_TURBO_MODULES because that property has core generate the
61
+ // call, and core is shared with hosts that cannot link this Skia -- see that
62
+ // package's CMakeLists. The same arrangement the 2.x module above has, and
63
+ // the same one main_appkit.mm makes.
64
+ #include "BasaltSkiaModule.h"
65
+ #endif
58
66
  #include "Win32Packaging.h"
59
67
  #include "PointerButtons.h"
68
+ #include "Win32AppWindow.h"
60
69
  #include "WindowControl.h"
61
70
  #include "SystemLocale.h"
62
71
  #include "WindowHost.h"
@@ -70,6 +79,7 @@
70
79
  #include "TextSelection.h"
71
80
  #include "Win32TouchDispatcher.h"
72
81
  #include "Win32UiThread.h"
82
+ #include "Win32UiTimer.h"
73
83
  #include "DragAndDrop.h"
74
84
  #include "Win32DropTarget.h"
75
85
  #include "Win32TitleBar.h"
@@ -162,16 +172,6 @@ constexpr int kInitialHeight = 700;
162
172
  // See e2e/demo.js. Only used when no module name is given.
163
173
  constexpr const char *kRenderFunctionName = "basaltRender";
164
174
 
165
- constexpr UINT_PTR kSecondTreeTimer = 100;
166
-
167
- // BASALT_QUIT_AFTER_MS's timer. A constant rather than the literal 101 it used
168
- // to be written as at its one call site: the spinner timer below was given 101
169
- // too, and `SetTimer` with an id that is already in use *replaces* the timer
170
- // rather than failing. An app with an <ActivityIndicator> in it therefore
171
- // silently cancelled its own shutdown, and the only symptom was a host that
172
- // never exited.
173
- constexpr UINT_PTR kQuitAfterTimer = 101;
174
-
175
175
  // One window, and the surface in it.
176
176
  //
177
177
  // A window is a surface: a surface is what has a size, a layout context and a
@@ -393,12 +393,16 @@ void requestRepaint() {
393
393
  // Win32AnimationChoreographer.h gives about its own timer: a timer that runs
394
394
  // whether or not anything is animating wakes the process sixty times a second
395
395
  // forever, which matters more on a laptop than the frame interval does.
396
- constexpr UINT_PTR kAnimationTimer = 102;
397
-
398
- // BASALT_TEST_QUIT_FILE's poll. Repeating, unlike kQuitAfterTimer's one shot:
399
- // it looks for the file until it finds it. See core/TestQuitFile.h.
400
- constexpr UINT_PTR kQuitFileTimer = 103;
401
- bool gAnimationTimerRunning = false;
396
+ //
397
+ // Not `SetTimer`, and this is the timer that proves why: the one thing it
398
+ // exists to do is repaint, and a `WM_TIMER` is the message Windows withholds
399
+ // while there is painting to do. A spinner that stops the moment the window
400
+ // gets busy is the exact failure. See win32/Win32UiTimer.h.
401
+ basalt::UiTimerId gAnimationTimer = 0;
402
+
403
+ // Defined below, and named here because the timer that calls it is started
404
+ // above it.
405
+ void tickAnimations(HWND hwnd);
402
406
  // When the last tick ran, so a frame delay is measured rather than assumed.
403
407
  // A GIF's delays are in hundredths of a second and the timer is 16ms, so
404
408
  // counting ticks would quantise every delay to a multiple of the tick and a
@@ -413,15 +417,16 @@ void updateAnimationTimer() {
413
417
  gHost.main().root->hasAnimatedImage() ||
414
418
  (gHost.logBoxRoot != nullptr &&
415
419
  (gHost.logBoxRoot->hasAnimatingSpinner() || gHost.logBoxRoot->hasAnimatedImage()));
416
- if (wanted == gAnimationTimerRunning) {
420
+ if (wanted == (gAnimationTimer != 0)) {
417
421
  return;
418
422
  }
419
- gAnimationTimerRunning = wanted;
420
423
  if (wanted) {
421
424
  gLastAnimationTick = std::chrono::steady_clock::now();
422
- SetTimer(gHost.main().window, kAnimationTimer, 16, nullptr);
425
+ HWND window = gHost.main().window;
426
+ gAnimationTimer = basalt::scheduleUiTimer(16, true, [window] { tickAnimations(window); });
423
427
  } else {
424
- KillTimer(gHost.main().window, kAnimationTimer);
428
+ basalt::cancelUiTimer(gAnimationTimer);
429
+ gAnimationTimer = 0;
425
430
  }
426
431
  }
427
432
 
@@ -566,6 +571,22 @@ facebook::react::TurboModuleProviders makeTurboModuleProviders(
566
571
  if (name == basalt::Win32SkiaModule::kModuleName) {
567
572
  return std::make_shared<basalt::Win32SkiaModule>(jsInvoker);
568
573
  }
574
+ #endif
575
+ #if BASALT_HAS_SKIA_GRAPHITE
576
+ // And 3.x, which is Graphite over Dawn and a different module under
577
+ // the same name. Never both: cmake/Skia.cmake refuses a 3.x install by
578
+ // version, so BASALT_HAS_SKIA is defined in exactly the builds this is
579
+ // not.
580
+ //
581
+ // Without this the canvas is built, linked and unreachable: the view
582
+ // mounts, basalt-skia's C++ is all in the binary, and the app still
583
+ // ends at getEnforcing('RNSkiaModule') on its first Skia import --
584
+ // which is where kino's first Windows run stopped.
585
+ if (name == basalt::BasaltSkiaModule::kModuleName) {
586
+ if (auto module = basalt::makeSkiaTurboModule(name, jsInvoker)) {
587
+ return module;
588
+ }
589
+ }
569
590
  #endif
570
591
  if (name == basalt::DesktopBlobModule::kModuleName) {
571
592
  return std::make_shared<basalt::DesktopBlobModule>(jsInvoker);
@@ -898,25 +919,18 @@ struct ScriptedInput {
898
919
  // Fired from timers keyed by index, so the vector has to outlive the loop.
899
920
  std::vector<ScriptedInput> gScriptedInput;
900
921
 
901
- constexpr UINT_PTR kScriptedInputTimerBase = 200;
902
-
903
- // Every timer id this host uses is distinct.
922
+ // There is no table of timer ids here any more, and that is the point.
923
+ //
924
+ // Every one of this host's waits used to be a `SetTimer` id written by hand,
925
+ // and `SetTimer` with an id already in use *replaces* the timer rather than
926
+ // failing -- so a collision was silent and showed up as whatever the replaced
927
+ // timer had been for. It happened: the spinner's repaint timer was given the
928
+ // same id as BASALT_QUIT_AFTER_MS's, and an app with an <ActivityIndicator>
929
+ // in it cancelled its own shutdown and hung. Two static_asserts guarded
930
+ // against the next one.
904
931
  //
905
- // `SetTimer` with an id that is already in use replaces the timer rather than
906
- // failing, so a collision is silent and its symptom is whatever the timer it
907
- // replaced was for. That happened: the spinner's repaint timer was given the
908
- // same id as BASALT_QUIT_AFTER_MS's, and an app with an <ActivityIndicator> in
909
- // it cancelled its own shutdown and hung. A static_assert cannot check the
910
- // scripted range against the rest, so the base is held above everything else.
911
- static_assert(kSecondTreeTimer != kQuitAfterTimer && kSecondTreeTimer != kAnimationTimer &&
912
- kSecondTreeTimer != kQuitFileTimer && kQuitAfterTimer != kAnimationTimer &&
913
- kQuitAfterTimer != kQuitFileTimer && kAnimationTimer != kQuitFileTimer,
914
- "two timers share an id, and SetTimer would silently replace one with the other");
915
- static_assert(kScriptedInputTimerBase > kSecondTreeTimer &&
916
- kScriptedInputTimerBase > kQuitAfterTimer &&
917
- kScriptedInputTimerBase > kAnimationTimer &&
918
- kScriptedInputTimerBase > kQuitFileTimer,
919
- "the scripted-input ids count up from the base and must start above the rest");
932
+ // win32/Win32UiTimer.h hands out its own ids from a counter, so there is
933
+ // nothing left to collide and nothing left to assert about.
920
934
 
921
935
  // The numbers in "10,20,30" -- however many there are, which is what lets one
922
936
  // parser read both a tap's pair and a drag's four.
@@ -938,9 +952,10 @@ std::vector<double> parseNumbers(const std::string &spec) {
938
952
  return numbers;
939
953
  }
940
954
 
941
- void CALLBACK fireScriptedInput(HWND hwnd, UINT, UINT_PTR id, DWORD) {
942
- KillTimer(hwnd, id);
943
- const size_t index = static_cast<size_t>(id - kScriptedInputTimerBase);
955
+ // One scheduled action, on the UI thread. An index and a window rather than a
956
+ // TIMERPROC's four arguments: the timer behind this carries a closure, so
957
+ // there is nothing to recover from an id.
958
+ void fireScriptedInput(HWND hwnd, size_t index) {
944
959
  if (index >= gScriptedInput.size()) {
945
960
  return;
946
961
  }
@@ -1159,10 +1174,13 @@ void CALLBACK fireScriptedInput(HWND hwnd, UINT, UINT_PTR id, DWORD) {
1159
1174
  // the last one, so a drag can be scheduled behind the taps.
1160
1175
  UINT scheduleScriptedInput(const ScriptedInput &action, UINT delayMs) {
1161
1176
  gScriptedInput.push_back(action);
1162
- SetTimer(gHost.main().window,
1163
- kScriptedInputTimerBase + gScriptedInput.size() - 1,
1164
- delayMs,
1165
- fireScriptedInput);
1177
+ const size_t index = gScriptedInput.size() - 1;
1178
+ HWND window = gHost.main().window;
1179
+ // Not `SetTimer`: scripted input exists to drive a *running* app, and the
1180
+ // apps worth driving are the ones that paint -- which is exactly when a
1181
+ // WM_TIMER never arrives. Every instrument in this file was silently
1182
+ // inoperative on an app with a `<Canvas>`. See win32/Win32UiTimer.h.
1183
+ basalt::scheduleUiTimer(delayMs, false, [window, index] { fireScriptedInput(window, index); });
1166
1184
  return delayMs + 1000;
1167
1185
  }
1168
1186
 
@@ -1440,7 +1458,12 @@ LRESULT CALLBACK hostProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
1440
1458
  // it needs its own case: WM_SIZE would never arrive, and `center()` would
1441
1459
  // look like it had done nothing.
1442
1460
  case WM_MOVE:
1443
- basalt::notifyWindowBoundsChanged();
1461
+ // Which window moved. `self` is null for the messages CreateWindowEx
1462
+ // sends before the window is in the list, and a move among those is not
1463
+ // one an app can see.
1464
+ if (self != nullptr) {
1465
+ basalt::notifyWindowBoundsChanged(self->surfaceId);
1466
+ }
1444
1467
  return 0;
1445
1468
 
1446
1469
  // `AppState`: whether this application is the one in front. Windows sends
@@ -1488,8 +1511,8 @@ LRESULT CALLBACK hostProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
1488
1511
  }
1489
1512
  // The window moved or was resized, which `useWindow()` hands an app as live
1490
1513
  // bounds. Here rather than in a watcher of its own: this is already the one
1491
- // place that learns it.
1492
- basalt::notifyWindowBoundsChanged();
1514
+ // place that learns it, and it already knows which window it was.
1515
+ basalt::notifyWindowBoundsChanged(self->surfaceId);
1493
1516
  // The inspector covers the main window, so it resizes with it -- and
1494
1517
  // only with it: an error is about the app rather than about a window.
1495
1518
  if (gHost.logBoxRoot != nullptr && self->surfaceId == kSurfaceId) {
@@ -1804,17 +1827,9 @@ LRESULT CALLBACK hostProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
1804
1827
  }
1805
1828
  return 0;
1806
1829
 
1807
- case WM_TIMER:
1808
- if (wparam == kAnimationTimer) {
1809
- tickAnimations(hwnd);
1810
- return 0;
1811
- }
1812
- if (wparam == kSecondTreeTimer) {
1813
- KillTimer(hwnd, kSecondTreeTimer);
1814
- std::fprintf(stderr, "--- committing tree 2 from JS ---\n");
1815
- callRenderFunction(2);
1816
- }
1817
- return 0;
1830
+ // No WM_TIMER case. Nothing this host schedules arrives that way any
1831
+ // more -- see win32/Win32UiTimer.h for why it could not be relied on --
1832
+ // and swallowing the message would hide a timer somebody else set.
1818
1833
 
1819
1834
  case WM_PAINT: {
1820
1835
  PAINTSTRUCT paint{};
@@ -2132,11 +2147,14 @@ facebook::react::SurfaceId openHostWindow(const NewWindowOptions &options) {
2132
2147
 
2133
2148
  gHost.reactHost->startSurface(surfaceId,
2134
2149
  options.component,
2135
- options.props,
2150
+ windowRootProps(options, surfaceId),
2136
2151
  constraintsFor(width, height),
2137
2152
  layoutContextFor(gHost.scaleFactor));
2138
2153
  ShowWindow(made->window, SW_SHOW);
2139
2154
  UpdateWindow(made->window);
2155
+ // Prime this window's bounds the way the main window's are primed at
2156
+ // startup; see the call beside that one.
2157
+ basalt::notifyWindowBoundsChanged(surfaceId);
2140
2158
  std::fprintf(stderr,
2141
2159
  "opened window %d for module %s\n",
2142
2160
  static_cast<int>(surfaceId),
@@ -2360,10 +2378,18 @@ int main(int argc, char **argv) {
2360
2378
  // being visible.
2361
2379
  gHost.mountingManager->setOnDidMount(syncPeersAndRepaint);
2362
2380
 
2381
+ // How the window module finds a window by id, and which one a call that
2382
+ // names none means. See Win32AppWindow.h.
2383
+ basalt::setWin32AppWindowLookup([](facebook::react::SurfaceId surfaceId) -> HWND {
2384
+ HostWindow *found = gHost.windowFor(surfaceId);
2385
+ return found == nullptr ? nullptr : found->window;
2386
+ });
2387
+ basalt::setAppWindowId(kSurfaceId);
2388
+
2363
2389
  // Prime the bounds cache, which `getBounds()` answers from: without this an
2364
2390
  // app's first render sees a window of no size, and only a later resize
2365
2391
  // corrects it. See core/WindowBoundsCache.cpp.
2366
- basalt::notifyWindowBoundsChanged();
2392
+ basalt::notifyWindowBoundsChanged(kSurfaceId);
2367
2393
 
2368
2394
  // And the display list, for the same reason and off the same seam: an app
2369
2395
  // asking which screens exist before anything has been plugged or unplugged
@@ -2529,7 +2555,10 @@ int main(int argc, char **argv) {
2529
2555
  if (gHost.moduleName.empty()) {
2530
2556
  std::fprintf(stderr, "--- committing tree 1 from JS ---\n");
2531
2557
  callRenderFunction(1);
2532
- SetTimer(gHost.main().window, kSecondTreeTimer, 2000, nullptr);
2558
+ basalt::scheduleUiTimer(2000, false, [] {
2559
+ std::fprintf(stderr, "--- committing tree 2 from JS ---\n");
2560
+ callRenderFunction(2);
2561
+ });
2533
2562
  }
2534
2563
 
2535
2564
  UINT scriptedDelayMs = 1500;
@@ -2643,21 +2672,20 @@ int main(int argc, char **argv) {
2643
2672
  // clicking anything. The same escape hatch both other hosts have.
2644
2673
  if (const char *quitAfter = std::getenv("BASALT_QUIT_AFTER_MS")) {
2645
2674
  const UINT delay = static_cast<UINT>(std::strtoul(quitAfter, nullptr, 10));
2646
- SetTimer(
2647
- gHost.main().window, kQuitAfterTimer, delay, [](HWND hwnd, UINT, UINT_PTR id, DWORD) {
2648
- KillTimer(hwnd, id);
2649
- // The same line the other two hosts log, so a person reading three
2650
- // transcripts side by side sees the same shutdown in each. This
2651
- // host was silent about it, which is how a scenario came to assert
2652
- // on a string only two of them print.
2653
- std::fprintf(stderr, "BASALT_QUIT_AFTER_MS elapsed; quitting\n");
2654
- // What WM_CLOSE does, without going through WM_CLOSE. This is the
2655
- // harness ending the run, not a person closing a window, and an app
2656
- // that intercepts its own close would otherwise refuse it -- a test
2657
- // that asks "does refusing work" by never exiting.
2658
- captureBeforeTeardown();
2659
- DestroyWindow(hwnd);
2660
- });
2675
+ HWND window = gHost.main().window;
2676
+ basalt::scheduleUiTimer(delay, false, [window] {
2677
+ // The same line the other two hosts log, so a person reading three
2678
+ // transcripts side by side sees the same shutdown in each. This
2679
+ // host was silent about it, which is how a scenario came to assert
2680
+ // on a string only two of them print.
2681
+ std::fprintf(stderr, "BASALT_QUIT_AFTER_MS elapsed; quitting\n");
2682
+ // What WM_CLOSE does, without going through WM_CLOSE. This is the
2683
+ // harness ending the run, not a person closing a window, and an app
2684
+ // that intercepts its own close would otherwise refuse it -- a test
2685
+ // that asks "does refusing work" by never exiting.
2686
+ captureBeforeTeardown();
2687
+ DestroyWindow(window);
2688
+ });
2661
2689
  }
2662
2690
 
2663
2691
  // BASALT_TEST_QUIT_FILE: the same shutdown, at a moment the harness picks
@@ -2671,26 +2699,25 @@ int main(int argc, char **argv) {
2671
2699
  //
2672
2700
  // Two instruments, one timer, because they want the same thing at different
2673
2701
  // moments: the quit file is the harness deciding, BASALT_QUIT_WHEN_SETTLED is
2674
- // the app noticing it has nothing left to do. One timer is also one id, and
2675
- // SetTimer replaces silently on a collision. See core/TestSettle.h.
2702
+ // the app noticing it has nothing left to do. See core/TestSettle.h.
2676
2703
  if (basalt::testQuitFilePath().has_value() || basalt::quitWhenSettledMs().has_value()) {
2677
- SetTimer(gHost.main().window,
2678
- kQuitFileTimer,
2679
- basalt::kTestQuitFilePollMs,
2680
- [](HWND hwnd, UINT, UINT_PTR id, DWORD) {
2681
- const char *why = nullptr;
2682
- if (basalt::testQuitFileAppeared()) {
2683
- why = basalt::kTestQuitFileMessage;
2684
- } else if (basalt::hasSettled()) {
2685
- why = basalt::kQuitWhenSettledMessage;
2686
- } else {
2687
- return;
2688
- }
2689
- KillTimer(hwnd, id);
2690
- std::fprintf(stderr, "%s\n", why);
2691
- captureBeforeTeardown();
2692
- DestroyWindow(hwnd);
2693
- });
2704
+ HWND window = gHost.main().window;
2705
+ static basalt::UiTimerId poll = 0;
2706
+ poll = basalt::scheduleUiTimer(basalt::kTestQuitFilePollMs, true, [window] {
2707
+ const char *why = nullptr;
2708
+ if (basalt::testQuitFileAppeared()) {
2709
+ why = basalt::kTestQuitFileMessage;
2710
+ } else if (basalt::hasSettled()) {
2711
+ why = basalt::kQuitWhenSettledMessage;
2712
+ } else {
2713
+ return;
2714
+ }
2715
+ basalt::cancelUiTimer(poll);
2716
+ poll = 0;
2717
+ std::fprintf(stderr, "%s\n", why);
2718
+ captureBeforeTeardown();
2719
+ DestroyWindow(window);
2720
+ });
2694
2721
  }
2695
2722
 
2696
2723
  // Not a plain GetMessage loop: the event beat has to be induced each time the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-win32",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "The Windows view layer for basalt-core: a Win32 and Direct2D view layer, a Fabric mounting manager, and a host. Everything behind them is in basalt-core.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,6 +22,6 @@
22
22
  "react-native": "*"
23
23
  },
24
24
  "dependencies": {
25
- "basalt-core": "0.2.1"
25
+ "basalt-core": "0.2.3"
26
26
  }
27
27
  }