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
@@ -0,0 +1,139 @@
1
+ #include "Win32UiTimer.h"
2
+
3
+ #include <windows.h>
4
+
5
+ #include "PlatformServices.h"
6
+ #include "Win32UiThread.h"
7
+
8
+ #include <mutex>
9
+ #include <unordered_map>
10
+ #include <utility>
11
+
12
+ namespace basalt {
13
+ namespace {
14
+
15
+ struct Entry {
16
+ std::function<void()> callback;
17
+ PTP_TIMER handle = nullptr;
18
+ bool repeating = false;
19
+ };
20
+
21
+ struct Registry {
22
+ std::mutex mutex;
23
+ std::unordered_map<UiTimerId, Entry> timers;
24
+ UiTimerId next = 1;
25
+ };
26
+
27
+ Registry &registry() {
28
+ static Registry instance;
29
+ return instance;
30
+ }
31
+
32
+ // Closing a thread-pool timer is three calls and the order is not optional:
33
+ // stop it, wait out a callback that may already be running on a pool thread,
34
+ // and only then release the handle. Skipping the wait is the classic way to
35
+ // free a timer out from under its own callback.
36
+ //
37
+ // Called with the registry lock *not* held: the wait blocks until a pool
38
+ // thread leaves `onExpiry`, and `onExpiry` takes that lock.
39
+ void closeTimer(PTP_TIMER handle) {
40
+ if (handle == nullptr) {
41
+ return;
42
+ }
43
+ SetThreadpoolTimer(handle, nullptr, 0, 0);
44
+ WaitForThreadpoolTimerCallbacks(handle, TRUE);
45
+ CloseThreadpoolTimer(handle);
46
+ }
47
+
48
+ // On the UI thread, one delay later. Everything about the timer is re-read
49
+ // here rather than captured, because the gap between the pool thread posting
50
+ // this and the loop running it is exactly when a cancel arrives.
51
+ void deliver(UiTimerId id) {
52
+ std::function<void()> callback;
53
+ PTP_TIMER finished = nullptr;
54
+ {
55
+ std::lock_guard<std::mutex> lock(registry().mutex);
56
+ const auto found = registry().timers.find(id);
57
+ if (found == registry().timers.end()) {
58
+ return;
59
+ }
60
+ callback = found->second.callback;
61
+ if (!found->second.repeating) {
62
+ // Out of the table before the callback runs, so that a one-shot which
63
+ // schedules its successor cannot collide with itself, and so that a
64
+ // `cancelUiTimer` on this id from inside the callback is the no-op the
65
+ // header promises.
66
+ finished = found->second.handle;
67
+ registry().timers.erase(found);
68
+ }
69
+ }
70
+ closeTimer(finished);
71
+ callback();
72
+ }
73
+
74
+ VOID CALLBACK onExpiry(PTP_CALLBACK_INSTANCE /*instance*/, PVOID context, PTP_TIMER /*timer*/) {
75
+ const auto id = static_cast<UiTimerId>(reinterpret_cast<std::uintptr_t>(context));
76
+ // Nothing is read here: the pool thread's only job is to say when. Which is
77
+ // also why the id travels rather than a pointer -- the entry may be gone by
78
+ // the time this lands.
79
+ postToUiThread([id] { deliver(id); });
80
+ }
81
+
82
+ } // namespace
83
+
84
+ UiTimerId scheduleUiTimer(unsigned delayMs, bool repeating, std::function<void()> callback) {
85
+ // See the header: with no loop to post to, a timer has nowhere to arrive,
86
+ // and pretending otherwise would run the callback on a pool thread.
87
+ if (!hasUiThread() || callback == nullptr) {
88
+ return 0;
89
+ }
90
+
91
+ UiTimerId id = 0;
92
+ {
93
+ std::lock_guard<std::mutex> lock(registry().mutex);
94
+ id = registry().next++;
95
+ }
96
+
97
+ PTP_TIMER handle = CreateThreadpoolTimer(
98
+ onExpiry, reinterpret_cast<PVOID>(static_cast<std::uintptr_t>(id)), nullptr);
99
+ if (handle == nullptr) {
100
+ return 0;
101
+ }
102
+
103
+ {
104
+ std::lock_guard<std::mutex> lock(registry().mutex);
105
+ registry().timers[id] = Entry{std::move(callback), handle, repeating};
106
+ }
107
+
108
+ // Negative is relative, in hundred-nanosecond units, which is the one piece
109
+ // of this API that reads like a mistake and is not.
110
+ LARGE_INTEGER relative{};
111
+ relative.QuadPart = -static_cast<LONGLONG>(delayMs) * 10000;
112
+ FILETIME due{};
113
+ due.dwLowDateTime = relative.LowPart;
114
+ due.dwHighDateTime = static_cast<DWORD>(relative.HighPart);
115
+ SetThreadpoolTimer(handle, &due, repeating ? static_cast<DWORD>(delayMs) : 0, 0);
116
+ return id;
117
+ }
118
+
119
+ void cancelUiTimer(UiTimerId id) {
120
+ if (id == 0) {
121
+ return;
122
+ }
123
+ PTP_TIMER handle = nullptr;
124
+ {
125
+ std::lock_guard<std::mutex> lock(registry().mutex);
126
+ const auto found = registry().timers.find(id);
127
+ if (found == registry().timers.end()) {
128
+ return;
129
+ }
130
+ handle = found->second.handle;
131
+ registry().timers.erase(found);
132
+ }
133
+ // Outside the lock, because the wait inside waits for `onExpiry` to finish
134
+ // and `onExpiry` wants the lock. A delivery already posted to the loop is
135
+ // harmless: `deliver` looks the id up and finds nothing.
136
+ closeTimer(handle);
137
+ }
138
+
139
+ } // namespace basalt
@@ -0,0 +1,65 @@
1
+ // A delay that arrives, in an app that is busy drawing.
2
+ //
3
+ // `SetTimer` is the obvious way to wait on Windows and it is not a reliable
4
+ // one. `WM_TIMER` is not posted: it is *synthesised* by `GetMessage` and
5
+ // `PeekMessage`, and only when the queue holds nothing else at all. It ranks
6
+ // below posted messages, below input, and -- the part that matters here --
7
+ // below `WM_PAINT`. An app that repaints whenever something changes is fine.
8
+ // An app that repaints continuously never gives the queue the empty moment a
9
+ // `WM_TIMER` needs, and its timers simply stop.
10
+ //
11
+ // That is not hypothetical and it is not rare. Measured on kino, an editor
12
+ // whose preview is a Skia `<Canvas>`: 120 `WM_PAINT` and 250 marshalled work
13
+ // items a second while *idle*, and six `WM_TIMER` in twenty-five seconds. A
14
+ // 250ms poll fired four times and then never again; a one-shot quit never
15
+ // fired at all, so every automated run of that app had to be killed. The
16
+ // other two desktops have no such ordering -- a CFRunLoop timer and a GLib
17
+ // source are both ordinary sources with no paint above them -- so this is a
18
+ // place where the Windows host was quietly worse than its peers rather than
19
+ // differently spelled.
20
+ //
21
+ // So the waiting happens off the UI thread, on a thread-pool timer, and the
22
+ // delivery is `postToUiThread` -- a posted message, which outranks both paint
23
+ // and timer, and which is the path every mount already takes. The callback
24
+ // still runs on the UI thread, which is the only property any caller wanted
25
+ // from `SetTimer` in the first place.
26
+ //
27
+ // ## Without a UI thread
28
+ //
29
+ // Nothing is delivered. A host installs the message-only window before React
30
+ // Native starts; the unit tests do not, because they have no loop. Under
31
+ // `SetTimer` that already meant "never fires" -- a `WM_TIMER` with nobody to
32
+ // dispatch it -- and keeping it that way is what lets the suite go on testing
33
+ // the code around these timers without growing a scheduler. A test that wants
34
+ // one installs a loop and pumps it, as the Skia canvas test does.
35
+
36
+ #pragma once
37
+
38
+ #include <cstdint>
39
+ #include <functional>
40
+
41
+ namespace basalt {
42
+
43
+ // What `scheduleUiTimer` hands back. Zero is "nothing was scheduled", which
44
+ // `cancelUiTimer` accepts, so a caller can keep one of these in a field and
45
+ // cancel it without asking whether it ever started.
46
+ using UiTimerId = std::uint64_t;
47
+
48
+ // Runs `callback` on the UI thread after `delayMs`, and every `delayMs`
49
+ // after that when `repeating`.
50
+ //
51
+ // The period is a floor rather than a promise, exactly as `SetTimer`'s was:
52
+ // the work is posted, so it arrives after whatever the loop is already
53
+ // holding. What is different is that it arrives.
54
+ UiTimerId scheduleUiTimer(unsigned delayMs, bool repeating, std::function<void()> callback);
55
+
56
+ // Stops it. Safe on an id that already fired, on one that was never
57
+ // scheduled, and on the running timer's own id from inside its callback --
58
+ // which is how a one-shot that reschedules itself is written.
59
+ //
60
+ // Returns having waited for any delivery already in flight on a pool thread,
61
+ // so a caller may destroy what the callback captured. It must be called from
62
+ // the UI thread, which every caller here is on.
63
+ void cancelUiTimer(UiTimerId id);
64
+
65
+ } // namespace basalt
@@ -21,17 +21,40 @@
21
21
  // scale factor. GetDpiForWindow would answer for the window's monitor only.
22
22
  #include <shellscalingapi.h>
23
23
 
24
+ #include "Win32AppWindow.h"
25
+
24
26
  namespace basalt {
25
27
 
26
28
  namespace {
27
29
 
30
+ // The window a call means, which is the lookup's answer where there is one.
31
+ HWND window(facebook::react::SurfaceId id) { return win32AppWindowFor(id); }
32
+
33
+ Win32AppWindowLookup &lookup() {
34
+ static Win32AppWindowLookup value;
35
+ return value;
36
+ }
37
+
38
+ } // namespace
28
39
 
29
- HWND window() {
40
+ HWND win32AppWindow() {
30
41
  HWND active = GetActiveWindow();
31
42
  return active != nullptr ? active : GetForegroundWindow();
32
43
  }
33
44
 
34
- } // namespace
45
+ void setWin32AppWindowLookup(Win32AppWindowLookup value) { lookup() = std::move(value); }
46
+
47
+ HWND win32AppWindowFor(facebook::react::SurfaceId id) {
48
+ if (id != kAppWindow && lookup()) {
49
+ if (HWND found = lookup()(id)) {
50
+ return found;
51
+ }
52
+ // An id nobody knows is the app's own window rather than nothing: a window
53
+ // that closed between a render and the call after it is the ordinary way
54
+ // to get here. See AppKitAppWindow.h, which says the same.
55
+ }
56
+ return win32AppWindow();
57
+ }
35
58
 
36
59
  // Every monitor, through the callback API that is the only way to ask.
37
60
  //
@@ -99,9 +122,9 @@ PointerPosition pointerPosition() {
99
122
  return where;
100
123
  }
101
124
 
102
- WindowBounds windowBounds() {
125
+ WindowBounds windowBounds(facebook::react::SurfaceId id) {
103
126
  WindowBounds bounds;
104
- HWND target = window();
127
+ HWND target = window(id);
105
128
  if (target == nullptr) {
106
129
  return bounds;
107
130
  }
@@ -138,8 +161,8 @@ WindowBounds windowBounds() {
138
161
  return bounds;
139
162
  }
140
163
 
141
- void setWindowSize(double width, double height) {
142
- HWND target = window();
164
+ void setWindowSize(facebook::react::SurfaceId id, double width, double height) {
165
+ HWND target = window(id);
143
166
  if (target == nullptr || width <= 0.0 || height <= 0.0) {
144
167
  return;
145
168
  }
@@ -160,8 +183,8 @@ void setWindowSize(double width, double height) {
160
183
  SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
161
184
  }
162
185
 
163
- void setWindowPosition(double x, double y) {
164
- HWND target = window();
186
+ void setWindowPosition(facebook::react::SurfaceId id, double x, double y) {
187
+ HWND target = window(id);
165
188
  if (target == nullptr) {
166
189
  return;
167
190
  }
@@ -174,8 +197,8 @@ void setWindowPosition(double x, double y) {
174
197
  SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
175
198
  }
176
199
 
177
- void centerWindow() {
178
- HWND target = window();
200
+ void centerWindow(facebook::react::SurfaceId id) {
201
+ HWND target = window(id);
179
202
  if (target == nullptr) {
180
203
  return;
181
204
  }
@@ -200,8 +223,8 @@ void centerWindow() {
200
223
  SetWindowPos(target, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
201
224
  }
202
225
 
203
- void setWindowFullScreen(bool fullScreen) {
204
- HWND target = window();
226
+ void setWindowFullScreen(facebook::react::SurfaceId id, bool fullScreen) {
227
+ HWND target = window(id);
205
228
  if (target == nullptr) {
206
229
  return;
207
230
  }
@@ -252,7 +275,7 @@ void setWindowFullScreen(bool fullScreen) {
252
275
  }
253
276
 
254
277
  void applyWindowSizeLimits() {
255
- HWND target = window();
278
+ HWND target = win32AppWindow();
256
279
  if (target == nullptr) {
257
280
  return;
258
281
  }
@@ -275,7 +298,7 @@ void applyWindowSizeLimits() {
275
298
  }
276
299
 
277
300
  void setWindowResizable(bool resizable) {
278
- HWND target = window();
301
+ HWND target = win32AppWindow();
279
302
  if (target == nullptr) {
280
303
  return;
281
304
  }
@@ -303,7 +326,7 @@ void setWindowResizable(bool resizable) {
303
326
  }
304
327
 
305
328
  void setWindowAlwaysOnTop(bool alwaysOnTop) {
306
- HWND target = window();
329
+ HWND target = win32AppWindow();
307
330
  if (target == nullptr) {
308
331
  return;
309
332
  }
@@ -96,24 +96,19 @@ Win32WindowModule::Win32WindowModule(std::shared_ptr<facebook::react::CallInvoke
96
96
  // The window's geometry, the same way the title bar's metrics go out: a
97
97
  // device event, so `useWindow()` can hand an app live bounds rather than a
98
98
  // snapshot it has to remember to refresh.
99
- setWindowBoundsListener([this](const WindowBounds &bounds) {
100
- emitDeviceEvent(kWindowBoundsEvent, [bounds](Runtime &runtime, std::vector<Value> &args) {
101
- Object object(runtime);
102
- object.setProperty(runtime, "x", bounds.x);
103
- object.setProperty(runtime, "y", bounds.y);
104
- object.setProperty(runtime, "width", bounds.width);
105
- object.setProperty(runtime, "height", bounds.height);
106
- object.setProperty(runtime, "fullScreen", bounds.fullScreen);
107
- object.setProperty(runtime, "maximized", bounds.maximized);
108
- args.emplace_back(Value(runtime, object));
109
- });
110
- });
99
+ setWindowBoundsListener(
100
+ [this](facebook::react::SurfaceId window, const WindowBounds &bounds) {
101
+ emitDeviceEvent(kWindowBoundsEvent,
102
+ [window, bounds](Runtime &runtime, std::vector<Value> &args) {
103
+ args.emplace_back(windowBoundsPayload(runtime, window, bounds));
104
+ });
105
+ });
111
106
 
112
107
  // And ask for the bounds once, now that something is listening. The host
113
108
  // primes the cache when its window appears, but this module is built lazily
114
109
  // -- the first time JavaScript asks for it -- so an app that mounts before
115
110
  // then would read a window of no size and never be told otherwise.
116
- postToUiThread([] { notifyWindowBoundsChanged(); });
111
+ postToUiThread([] { notifyWindowBoundsChanged(resolveWindow(kAppWindow)); });
117
112
  }
118
113
 
119
114
  Win32WindowModule::~Win32WindowModule() {