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.
- package/native/CMakeLists.txt +56 -9
- package/native/tests/test_win32_accessibility.cpp +77 -0
- package/native/tests/test_win32_focus.cpp +26 -0
- package/native/tests/test_win32_input.cpp +82 -0
- package/native/tests/test_win32_peer_clip.cpp +266 -0
- package/native/tests/test_win32_run_loop.cpp +122 -0
- package/native/tests/test_win32_textinput.cpp +199 -0
- package/native/tests/test_win32_ui_timer.cpp +195 -0
- package/native/win32/PlatformServicesWin32.cpp +16 -0
- package/native/win32/RnWin32Accessible.cpp +27 -0
- package/native/win32/RnWin32Accessible.h +5 -0
- package/native/win32/RnWin32View.h +18 -0
- package/native/win32/Win32AnimationChoreographer.cpp +15 -32
- package/native/win32/Win32AnimationChoreographer.h +9 -6
- package/native/win32/Win32AppWindow.h +36 -0
- package/native/win32/Win32Cursors.cpp +13 -17
- package/native/win32/Win32DropTarget.cpp +4 -1
- package/native/win32/Win32MenuBar.cpp +3 -1
- package/native/win32/Win32MountingManager.cpp +82 -7
- package/native/win32/Win32MountingManager.h +5 -0
- package/native/win32/Win32PeerClip.cpp +153 -0
- package/native/win32/Win32PeerClip.h +92 -0
- package/native/win32/Win32RunLoopObserver.cpp +39 -9
- package/native/win32/Win32RunLoopObserver.h +41 -2
- package/native/win32/Win32ScrollView.cpp +13 -32
- package/native/win32/Win32ScrollView.h +5 -6
- package/native/win32/Win32SkiaPeer.h +26 -4
- package/native/win32/Win32TextInput.cpp +139 -48
- package/native/win32/Win32TextInput.h +28 -8
- package/native/win32/Win32UiThread.h +11 -0
- package/native/win32/Win32UiTimer.cpp +139 -0
- package/native/win32/Win32UiTimer.h +65 -0
- package/native/win32/Win32WindowControl.cpp +38 -15
- package/native/win32/Win32WindowModule.cpp +8 -13
- package/native/win32/main_win32.cpp +123 -96
- 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 ®istry() {
|
|
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
|
|
40
|
+
HWND win32AppWindow() {
|
|
30
41
|
HWND active = GetActiveWindow();
|
|
31
42
|
return active != nullptr ? active : GetForegroundWindow();
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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() {
|