basalt-win32 0.2.1 → 0.2.2
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_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 +108 -0
- package/native/tests/test_win32_ui_timer.cpp +195 -0
- package/native/win32/PlatformServicesWin32.cpp +16 -0
- package/native/win32/RnWin32View.h +7 -0
- package/native/win32/Win32AnimationChoreographer.cpp +15 -32
- package/native/win32/Win32AnimationChoreographer.h +9 -6
- package/native/win32/Win32Cursors.cpp +13 -17
- package/native/win32/Win32MountingManager.cpp +6 -3
- 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 +106 -46
- package/native/win32/Win32TextInput.h +23 -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/main_win32.cpp +101 -91
- package/package.json +2 -2
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
#include <cstdint>
|
|
35
35
|
#include <memory>
|
|
36
|
+
#include <optional>
|
|
37
|
+
#include <sstream>
|
|
36
38
|
#include <string>
|
|
37
39
|
#include <utility>
|
|
38
40
|
#include <vector>
|
|
@@ -326,6 +328,34 @@ RECT rectOf(HWND control) {
|
|
|
326
328
|
return rect;
|
|
327
329
|
}
|
|
328
330
|
|
|
331
|
+
// The window region installed on a control, as the box around it, in the
|
|
332
|
+
// control's own coordinates. Absent when the control has no region at all,
|
|
333
|
+
// which is what a whole unobstructed field gets.
|
|
334
|
+
//
|
|
335
|
+
// An empty box -- `0,0 0,0` -- is a region with nothing in it, which is a
|
|
336
|
+
// field that is entirely behind something.
|
|
337
|
+
std::optional<RECT> regionBoxOf(HWND control) {
|
|
338
|
+
HRGN region = CreateRectRgn(0, 0, 0, 0);
|
|
339
|
+
const int kind = GetWindowRgn(control, region);
|
|
340
|
+
std::optional<RECT> box;
|
|
341
|
+
// GDI's `ERROR` is zero, and the name is not reachable here: this build
|
|
342
|
+
// defines GLOG_NO_ABBREVIATED_SEVERITIES, which is what keeps glog from
|
|
343
|
+
// fighting wingdi.h over it, and wingdi.h's own definition loses to it.
|
|
344
|
+
if (kind != 0) {
|
|
345
|
+
RECT rect{0, 0, 0, 0};
|
|
346
|
+
GetRgnBox(region, &rect);
|
|
347
|
+
box = rect;
|
|
348
|
+
}
|
|
349
|
+
DeleteObject(region);
|
|
350
|
+
return box;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
std::string describeRect(const RECT &rect) {
|
|
354
|
+
std::ostringstream out;
|
|
355
|
+
out << rect.left << "," << rect.top << " " << rect.right << "," << rect.bottom;
|
|
356
|
+
return out.str();
|
|
357
|
+
}
|
|
358
|
+
|
|
329
359
|
} // namespace
|
|
330
360
|
|
|
331
361
|
// A mounted field gets a real control, and the host's window is its parent.
|
|
@@ -576,6 +606,84 @@ TEST(win32_a_peer_scrolled_out_of_view_is_hidden) {
|
|
|
576
606
|
manager->destroySurfaceRoot(kSurfaceId);
|
|
577
607
|
}
|
|
578
608
|
|
|
609
|
+
// Half out of its list is half drawn, which hiding it entirely was never the
|
|
610
|
+
// same as. A window region is what says so to USER32; see Win32PeerClip.h.
|
|
611
|
+
//
|
|
612
|
+
// The field is multiline so that the control fills its box exactly -- a
|
|
613
|
+
// single-line peer is a strip the height of one line, centred, and these
|
|
614
|
+
// numbers would then be the font's.
|
|
615
|
+
TEST(win32_a_peer_half_out_of_its_list_is_clipped_to_it) {
|
|
616
|
+
auto manager = makeManager();
|
|
617
|
+
RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
|
|
618
|
+
root->setFrame(0, 0, 800, 600);
|
|
619
|
+
mount(*manager, kSurfaceId, makeView(10, 0, 0, 400, 100));
|
|
620
|
+
manager->viewForTag(10)->setClipsChildren(true);
|
|
621
|
+
mount(*manager, 10, makeField(11, 0, 60, 200, 80, {.multiline = true}));
|
|
622
|
+
manager->syncTextInputBounds(root);
|
|
623
|
+
|
|
624
|
+
HWND control = peerOf(manager->viewForTag(11));
|
|
625
|
+
EXPECT_EQ(describeRect(rectOf(control)), std::string("0,60 200,140"));
|
|
626
|
+
// The list ends at y=100, so the bottom 40 of the field is not the field's.
|
|
627
|
+
const auto clipped = regionBoxOf(control);
|
|
628
|
+
EXPECT(clipped.has_value());
|
|
629
|
+
EXPECT_EQ(describeRect(clipped.value_or(RECT{})), std::string("0,0 200,40"));
|
|
630
|
+
|
|
631
|
+
// Scrolled until it is wholly inside, the region goes away rather than
|
|
632
|
+
// becoming the whole control: SetWindowRgn redraws, and a field in a list
|
|
633
|
+
// being scrolled would otherwise be told to on every notch.
|
|
634
|
+
manager->viewForTag(10)->setScrollOffset(0, 60);
|
|
635
|
+
manager->syncTextInputBounds(root);
|
|
636
|
+
EXPECT(!regionBoxOf(control).has_value());
|
|
637
|
+
|
|
638
|
+
manager->destroySurfaceRoot(kSurfaceId);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// A field cannot be covered by anything the tree paints after it, because the
|
|
642
|
+
// peer is a child window and USER32 draws those last. Open a modal over a pane
|
|
643
|
+
// with a field in it and the field floats on top of the modal, still editable.
|
|
644
|
+
// Taking the modal's box out of the region is what stops that.
|
|
645
|
+
TEST(win32_a_peer_under_something_painted_later_is_cut_away) {
|
|
646
|
+
auto manager = makeManager();
|
|
647
|
+
RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
|
|
648
|
+
root->setFrame(0, 0, 800, 600);
|
|
649
|
+
// The backdrop is mounted first and the field second, and both go in at
|
|
650
|
+
// index 0 -- so the children end up [field, backdrop] and the backdrop is
|
|
651
|
+
// painted after, which is where an overlay sits in a React tree.
|
|
652
|
+
mount(*manager, kSurfaceId, makeView(10, 0, 0, 800, 600));
|
|
653
|
+
mount(*manager, kSurfaceId, makeField(11, 0, 0, 200, 80, {.multiline = true}));
|
|
654
|
+
manager->syncTextInputBounds(root);
|
|
655
|
+
|
|
656
|
+
HWND control = peerOf(manager->viewForTag(11));
|
|
657
|
+
// A layout wrapper with no background of its own covers nothing, which is
|
|
658
|
+
// most of a real tree and the reason this rule is quiet in one.
|
|
659
|
+
EXPECT(!regionBoxOf(control).has_value());
|
|
660
|
+
|
|
661
|
+
// kino's modal scrim: #000000A0 over the whole window. Translucent counts --
|
|
662
|
+
// a child window cannot be dimmed by paint, so the choice is between showing
|
|
663
|
+
// it undimmed and hiding it, and hiding it is the better wrong answer.
|
|
664
|
+
manager->viewForTag(10)->setBackgroundColor(0.0f, 0.0f, 0.0f, 0.63f, true);
|
|
665
|
+
manager->syncTextInputBounds(root);
|
|
666
|
+
// Wholly behind it is hidden rather than given a region with nothing in it:
|
|
667
|
+
// the two look the same and only one of them stops Windows thinking there is
|
|
668
|
+
// a window there.
|
|
669
|
+
EXPECT(!IsWindowVisible(control));
|
|
670
|
+
EXPECT(!regionBoxOf(control).has_value());
|
|
671
|
+
|
|
672
|
+
// Half over it leaves the other half.
|
|
673
|
+
manager->viewForTag(10)->setFrame(0, 40, 800, 600);
|
|
674
|
+
manager->syncTextInputBounds(root);
|
|
675
|
+
const auto half = regionBoxOf(control);
|
|
676
|
+
EXPECT(half.has_value());
|
|
677
|
+
EXPECT_EQ(describeRect(half.value_or(RECT{})), std::string("0,0 200,40"));
|
|
678
|
+
|
|
679
|
+
// And once the modal closes the region goes with it.
|
|
680
|
+
manager->viewForTag(10)->setBackgroundColor(0.0f, 0.0f, 0.0f, 0.0f, false);
|
|
681
|
+
manager->syncTextInputBounds(root);
|
|
682
|
+
EXPECT(!regionBoxOf(control).has_value());
|
|
683
|
+
|
|
684
|
+
manager->destroySurfaceRoot(kSurfaceId);
|
|
685
|
+
}
|
|
686
|
+
|
|
579
687
|
// focus, blur and setTextAndSelection, which arrive through dispatchCommand.
|
|
580
688
|
TEST(win32_the_commands_reach_the_control) {
|
|
581
689
|
auto manager = makeManager();
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Timers that arrive, including under the one condition that broke the ones
|
|
2
|
+
// before them. See win32/Win32UiTimer.h.
|
|
3
|
+
|
|
4
|
+
#include "TestHarness.h"
|
|
5
|
+
|
|
6
|
+
#include "Win32UiThread.h"
|
|
7
|
+
#include "Win32UiTimer.h"
|
|
8
|
+
|
|
9
|
+
#include <windows.h>
|
|
10
|
+
|
|
11
|
+
#include <chrono>
|
|
12
|
+
#include <functional>
|
|
13
|
+
#include <thread>
|
|
14
|
+
|
|
15
|
+
namespace {
|
|
16
|
+
|
|
17
|
+
// A real message loop for the duration of one test. Without one there is
|
|
18
|
+
// nothing to post to, `scheduleUiTimer` declines, and every assertion below
|
|
19
|
+
// would be about the declining rather than about the timer.
|
|
20
|
+
struct ScopedUiThread {
|
|
21
|
+
ScopedUiThread() { basalt::installUiThread(); }
|
|
22
|
+
~ScopedUiThread() { basalt::uninstallUiThread(); }
|
|
23
|
+
ScopedUiThread(const ScopedUiThread &) = delete;
|
|
24
|
+
ScopedUiThread &operator=(const ScopedUiThread &) = delete;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Pumps until `ready`, or until the deadline. Returns whether it came true.
|
|
28
|
+
//
|
|
29
|
+
// The inner drain is bounded rather than "until the queue is empty", which
|
|
30
|
+
// the real loop can afford and this cannot: one test here feeds the queue a
|
|
31
|
+
// message that reposts itself, and an unbounded drain never comes back out.
|
|
32
|
+
bool pumpUntil(const std::function<bool()> &ready) {
|
|
33
|
+
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
|
34
|
+
while (std::chrono::steady_clock::now() < deadline) {
|
|
35
|
+
MSG message;
|
|
36
|
+
for (int drained = 0; drained < 64; drained++) {
|
|
37
|
+
if (!PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
TranslateMessage(&message);
|
|
41
|
+
DispatchMessageW(&message);
|
|
42
|
+
}
|
|
43
|
+
if (ready()) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
} // namespace
|
|
52
|
+
|
|
53
|
+
TEST(ui_timer_fires_once_after_its_delay) {
|
|
54
|
+
ScopedUiThread loop;
|
|
55
|
+
int fired = 0;
|
|
56
|
+
const basalt::UiTimerId timer = basalt::scheduleUiTimer(10, false, [&fired] { fired++; });
|
|
57
|
+
EXPECT(timer != 0);
|
|
58
|
+
EXPECT(pumpUntil([&fired] { return fired > 0; }));
|
|
59
|
+
// And exactly once: a one-shot that repeated would be found by nothing else
|
|
60
|
+
// here, because every other assertion is satisfied by the first call.
|
|
61
|
+
const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(120);
|
|
62
|
+
pumpUntil([&until] { return std::chrono::steady_clock::now() > until; });
|
|
63
|
+
EXPECT_EQ(fired, 1);
|
|
64
|
+
basalt::cancelUiTimer(timer);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
TEST(ui_timer_repeats_until_it_is_cancelled) {
|
|
68
|
+
ScopedUiThread loop;
|
|
69
|
+
int fired = 0;
|
|
70
|
+
const basalt::UiTimerId timer = basalt::scheduleUiTimer(5, true, [&fired] { fired++; });
|
|
71
|
+
EXPECT(timer != 0);
|
|
72
|
+
EXPECT(pumpUntil([&fired] { return fired >= 3; }));
|
|
73
|
+
basalt::cancelUiTimer(timer);
|
|
74
|
+
const int afterCancel = fired;
|
|
75
|
+
const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(80);
|
|
76
|
+
pumpUntil([&until] { return std::chrono::steady_clock::now() > until; });
|
|
77
|
+
EXPECT_EQ(fired, afterCancel);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
TEST(ui_timer_can_be_cancelled_from_inside_its_own_callback) {
|
|
81
|
+
// How a repeating animation ends: the last frame stops the thing that
|
|
82
|
+
// delivered it. `cancelUiTimer` waits out a pool-thread delivery, so doing
|
|
83
|
+
// this on the wrong thread would deadlock rather than misbehave.
|
|
84
|
+
ScopedUiThread loop;
|
|
85
|
+
int fired = 0;
|
|
86
|
+
basalt::UiTimerId timer = 0;
|
|
87
|
+
timer = basalt::scheduleUiTimer(5, true, [&fired, &timer] {
|
|
88
|
+
fired++;
|
|
89
|
+
if (fired == 2) {
|
|
90
|
+
basalt::cancelUiTimer(timer);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
EXPECT(pumpUntil([&fired] { return fired >= 2; }));
|
|
94
|
+
const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(80);
|
|
95
|
+
pumpUntil([&until] { return std::chrono::steady_clock::now() > until; });
|
|
96
|
+
EXPECT_EQ(fired, 2);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
TEST(ui_timer_arrives_on_a_queue_that_never_empties) {
|
|
100
|
+
// The whole reason this file exists.
|
|
101
|
+
//
|
|
102
|
+
// `WM_TIMER` is not posted. It is *synthesised* by `PeekMessage`, and only
|
|
103
|
+
// once the queue holds nothing else -- below posted messages, below input,
|
|
104
|
+
// below `WM_PAINT`. A loop that always has something to do therefore never
|
|
105
|
+
// gives a `WM_TIMER` the gap it needs, and every `SetTimer` in the host
|
|
106
|
+
// simply stopped. That is the ordinary condition of a real app: measured on
|
|
107
|
+
// kino, whose preview is a Skia `<Canvas>`, 120 `WM_PAINT` and 250 posted
|
|
108
|
+
// work items a second while idle, against six `WM_TIMER` in twenty-five
|
|
109
|
+
// seconds.
|
|
110
|
+
//
|
|
111
|
+
// Reproduced here with a message that reposts itself, which is the same
|
|
112
|
+
// queue condition as the painting without needing a window on screen.
|
|
113
|
+
ScopedUiThread loop;
|
|
114
|
+
|
|
115
|
+
constexpr UINT kFlood = WM_APP + 7;
|
|
116
|
+
static long flooded = 0;
|
|
117
|
+
static long ticked = 0;
|
|
118
|
+
// Cleared before the window goes, so the self-reposting message stops
|
|
119
|
+
// rather than outliving this test in the queue the next one pumps.
|
|
120
|
+
static bool flooding = true;
|
|
121
|
+
flooded = 0;
|
|
122
|
+
ticked = 0;
|
|
123
|
+
flooding = true;
|
|
124
|
+
|
|
125
|
+
WNDCLASSEXW windowClass{};
|
|
126
|
+
windowClass.cbSize = sizeof(windowClass);
|
|
127
|
+
windowClass.lpfnWndProc = [](HWND hwnd, UINT message, WPARAM w, LPARAM l) -> LRESULT {
|
|
128
|
+
if (message == kFlood) {
|
|
129
|
+
flooded++;
|
|
130
|
+
// Posted again before this one is finished with, so the queue is never
|
|
131
|
+
// observed empty. Which is the condition under test.
|
|
132
|
+
if (flooding) {
|
|
133
|
+
PostMessageW(hwnd, kFlood, 0, 0);
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
if (message == WM_TIMER) {
|
|
138
|
+
ticked++;
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
return DefWindowProcW(hwnd, message, w, l);
|
|
142
|
+
};
|
|
143
|
+
windowClass.hInstance = GetModuleHandleW(nullptr);
|
|
144
|
+
windowClass.lpszClassName = L"BasaltUiTimerBusyTest";
|
|
145
|
+
RegisterClassExW(&windowClass);
|
|
146
|
+
HWND window = CreateWindowExW(0,
|
|
147
|
+
L"BasaltUiTimerBusyTest",
|
|
148
|
+
L"",
|
|
149
|
+
0,
|
|
150
|
+
0,
|
|
151
|
+
0,
|
|
152
|
+
0,
|
|
153
|
+
0,
|
|
154
|
+
HWND_MESSAGE,
|
|
155
|
+
nullptr,
|
|
156
|
+
GetModuleHandleW(nullptr),
|
|
157
|
+
nullptr);
|
|
158
|
+
EXPECT(window != nullptr);
|
|
159
|
+
PostMessageW(window, kFlood, 0, 0);
|
|
160
|
+
|
|
161
|
+
// The platform's own timer, for the comparison. Ten milliseconds, so a
|
|
162
|
+
// working one would fire many times over what follows.
|
|
163
|
+
SetTimer(window, 1, 10, nullptr);
|
|
164
|
+
|
|
165
|
+
int fired = 0;
|
|
166
|
+
const basalt::UiTimerId timer = basalt::scheduleUiTimer(10, false, [&fired] { fired++; });
|
|
167
|
+
EXPECT(pumpUntil([&fired] { return fired > 0; }));
|
|
168
|
+
|
|
169
|
+
// The flood really did run, so the pass above means the timer arrived
|
|
170
|
+
// through a busy queue rather than through a quiet one.
|
|
171
|
+
EXPECT(flooded > 100);
|
|
172
|
+
// ...and the comparison. If this ever starts firing, Windows has changed
|
|
173
|
+
// its message priorities and this whole file is worth re-reading rather
|
|
174
|
+
// than the assertion being worth relaxing.
|
|
175
|
+
EXPECT_EQ(static_cast<int>(ticked), 0);
|
|
176
|
+
|
|
177
|
+
flooding = false;
|
|
178
|
+
KillTimer(window, 1);
|
|
179
|
+
basalt::cancelUiTimer(timer);
|
|
180
|
+
DestroyWindow(window);
|
|
181
|
+
UnregisterClassW(L"BasaltUiTimerBusyTest", GetModuleHandleW(nullptr));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
TEST(ui_timer_declines_when_there_is_no_loop_to_post_to) {
|
|
185
|
+
// No ScopedUiThread. Delivering here would mean running the callback on a
|
|
186
|
+
// thread pool, which is how the rest of this suite -- written against
|
|
187
|
+
// inline posting on one thread -- would start failing at random.
|
|
188
|
+
int fired = 0;
|
|
189
|
+
EXPECT_EQ(static_cast<int>(basalt::scheduleUiTimer(1, false, [&fired] { fired++; })), 0);
|
|
190
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
|
191
|
+
EXPECT_EQ(fired, 0);
|
|
192
|
+
// And cancelling nothing is allowed, which is what lets a caller keep one
|
|
193
|
+
// id in a field and cancel it without asking whether it ever started.
|
|
194
|
+
basalt::cancelUiTimer(0);
|
|
195
|
+
}
|
|
@@ -116,6 +116,22 @@ bool hasUiThread() {
|
|
|
116
116
|
return uiThread().window != nullptr;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
void uninstallUiThread() {
|
|
120
|
+
HWND window = uiThread().window;
|
|
121
|
+
if (window == nullptr) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// Cleared before the window is destroyed, so that anything posted from
|
|
125
|
+
// another thread between these two lines runs inline rather than through a
|
|
126
|
+
// PostMessage to a handle that is going away.
|
|
127
|
+
uiThread().window = nullptr;
|
|
128
|
+
uiThread().threadId = std::thread::id{};
|
|
129
|
+
DestroyWindow(window);
|
|
130
|
+
// And whatever was queued and never pumped, which is now nobody's: running
|
|
131
|
+
// it here is what the no-window path would have done with it anyway.
|
|
132
|
+
drainPendingWork();
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
bool isUiThread() {
|
|
120
136
|
// With no UI thread installed there is only one thread that matters, so
|
|
121
137
|
// saying yes is both true and what keeps the tests from marshalling into a
|
|
@@ -160,6 +160,13 @@ class RnWin32View {
|
|
|
160
160
|
// as transparent black: a view with no background does not paint at all.
|
|
161
161
|
void setBackgroundColor(float red, float green, float blue, float alpha, bool hasColor);
|
|
162
162
|
bool hasBackgroundColor() const { return hasBackgroundColor_; }
|
|
163
|
+
// The four components, in the order they were set. Only meaningful when
|
|
164
|
+
// `hasBackgroundColor()`; a view without one has never written them.
|
|
165
|
+
//
|
|
166
|
+
// Read by Win32PeerClip, which has to know whether a view painted over a
|
|
167
|
+
// `<TextInput>` covers it, and that is the alpha. Painting itself does not
|
|
168
|
+
// need the accessor -- it is a member and reads the array directly.
|
|
169
|
+
const float *backgroundColor() const { return backgroundColor_; }
|
|
163
170
|
|
|
164
171
|
void setOpacity(float opacity);
|
|
165
172
|
float opacity() const { return opacity_; }
|
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
#include "Win32AnimationChoreographer.h"
|
|
2
2
|
|
|
3
|
-
#include
|
|
3
|
+
#include "Win32UiTimer.h"
|
|
4
4
|
|
|
5
5
|
#include <chrono>
|
|
6
6
|
|
|
7
7
|
namespace basalt {
|
|
8
8
|
namespace {
|
|
9
9
|
|
|
10
|
-
// Roughly sixty hertz.
|
|
11
|
-
//
|
|
12
|
-
// so this is an upper bound on the frame rate
|
|
13
|
-
//
|
|
14
|
-
constexpr
|
|
15
|
-
|
|
16
|
-
// SetTimer's callback carries the timer id and nothing else, so the
|
|
17
|
-
// choreographer has to be found from it. One host means one choreographer, and
|
|
18
|
-
// a map would be pretending otherwise.
|
|
19
|
-
Win32AnimationChoreographer *¤tChoreographer() {
|
|
20
|
-
static Win32AnimationChoreographer *instance = nullptr;
|
|
21
|
-
return instance;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
void CALLBACK onTimer(HWND, UINT, UINT_PTR, DWORD) {
|
|
25
|
-
if (auto *choreographer = currentChoreographer()) {
|
|
26
|
-
choreographer->onFrame();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
10
|
+
// Roughly sixty hertz. The delay is a floor rather than a promise -- the frame
|
|
11
|
+
// is posted to the loop and arrives behind whatever the loop is already
|
|
12
|
+
// holding -- so this is an upper bound on the frame rate. See the header for
|
|
13
|
+
// what would be needed to do it properly.
|
|
14
|
+
constexpr unsigned kFrameIntervalMs = 16;
|
|
29
15
|
|
|
30
16
|
} // namespace
|
|
31
17
|
|
|
@@ -37,28 +23,25 @@ void Win32AnimationChoreographer::resume() {
|
|
|
37
23
|
if (running_.exchange(true)) {
|
|
38
24
|
return;
|
|
39
25
|
}
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
timer_ = static_cast<std::uintptr_t>(SetTimer(nullptr, 0, kFrameIntervalMs, onTimer));
|
|
26
|
+
// Not `SetTimer`. A sixty-hertz WM_TIMER is exactly the arrangement that
|
|
27
|
+
// stops working in an app that repaints continuously, which is to say in
|
|
28
|
+
// every app with an animation worth running. See win32/Win32UiTimer.h.
|
|
29
|
+
timer_ = scheduleUiTimer(kFrameIntervalMs, true, [this] { onFrame(); });
|
|
45
30
|
}
|
|
46
31
|
|
|
47
32
|
void Win32AnimationChoreographer::pause() {
|
|
48
33
|
if (!running_.exchange(false)) {
|
|
49
34
|
return;
|
|
50
35
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
36
|
+
// Before this returns, nothing of ours is still in flight: `cancelUiTimer`
|
|
37
|
+
// waits out a delivery already on a pool thread, which is what makes the
|
|
38
|
+
// `this` captured above safe to destroy afterwards.
|
|
39
|
+
cancelUiTimer(timer_);
|
|
40
|
+
timer_ = 0;
|
|
55
41
|
}
|
|
56
42
|
|
|
57
43
|
void Win32AnimationChoreographer::detach() {
|
|
58
44
|
pause();
|
|
59
|
-
if (currentChoreographer() == this) {
|
|
60
|
-
currentChoreographer() = nullptr;
|
|
61
|
-
}
|
|
62
45
|
}
|
|
63
46
|
|
|
64
47
|
void Win32AnimationChoreographer::onFrame() {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// own thread, because DwmFlush blocks, and a blocked UI thread is worse than a
|
|
13
13
|
// slightly wrong frame interval.
|
|
14
14
|
//
|
|
15
|
-
// So this is a
|
|
16
|
-
//
|
|
15
|
+
// So this is a timer at sixty hertz, and it is worth being clear that this is
|
|
16
|
+
// a placeholder rather than a design. `docs/BACKLOG.md` already
|
|
17
17
|
// records that worklets and Reanimated run on a sixteen-millisecond timer on
|
|
18
18
|
// *both* other desktops for a related reason -- each platform's display link
|
|
19
19
|
// belongs to React Native's own choreographer and pauses when React Native has
|
|
@@ -52,10 +52,13 @@ class Win32AnimationChoreographer final : public facebook::react::AnimationChore
|
|
|
52
52
|
|
|
53
53
|
private:
|
|
54
54
|
std::atomic<bool> running_{false};
|
|
55
|
-
// A
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
55
|
+
// A `basalt::UiTimerId` rather than a `SetTimer` handle, and not for
|
|
56
|
+
// tidiness: WM_TIMER ranks below WM_PAINT, so on this timer an app that
|
|
57
|
+
// repaints continuously -- anything with a `<Canvas>` -- stopped getting
|
|
58
|
+
// animation frames at all. See win32/Win32UiTimer.h. Held as a plain
|
|
59
|
+
// integer so this header needs no windows.h, which would otherwise reach
|
|
60
|
+
// every translation unit that constructs a host.
|
|
61
|
+
std::uint64_t timer_{0};
|
|
59
62
|
};
|
|
60
63
|
|
|
61
64
|
} // namespace basalt
|
|
@@ -244,20 +244,6 @@ HCURSOR cursorFromArt(const Art &art, int hotX, int hotY) {
|
|
|
244
244
|
return cursor;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
// `%SystemRoot%\Cursors\aero_link.cur`, the shell's own shortcut cursor, which
|
|
248
|
-
// is the one of the six Windows ships as a file with a name rather than as an
|
|
249
|
-
// ordinal inside a DLL. Null when it is not there, and the caller draws its own.
|
|
250
|
-
HCURSOR cursorFromSystemFile(const wchar_t *leaf) {
|
|
251
|
-
wchar_t path[MAX_PATH]{};
|
|
252
|
-
const UINT length = GetWindowsDirectoryW(path, MAX_PATH);
|
|
253
|
-
if (length == 0 || length >= MAX_PATH) {
|
|
254
|
-
return nullptr;
|
|
255
|
-
}
|
|
256
|
-
std::wstring full(path);
|
|
257
|
-
full += L"\\Cursors\\";
|
|
258
|
-
full += leaf;
|
|
259
|
-
return LoadCursorFromFileW(full.c_str());
|
|
260
|
-
}
|
|
261
247
|
|
|
262
248
|
HCURSOR drawnCursorFor(const std::string &keyword) {
|
|
263
249
|
if (keyword == "copy") {
|
|
@@ -267,9 +253,19 @@ HCURSOR drawnCursorFor(const std::string &keyword) {
|
|
|
267
253
|
return cursorFromArt(contextMenuArt(), 0, 0);
|
|
268
254
|
}
|
|
269
255
|
if (keyword == "alias") {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
256
|
+
// Drawn, like the other five, and not `%SystemRoot%\Cursors\aero_link.cur`.
|
|
257
|
+
//
|
|
258
|
+
// That file was preferred here on the reasoning that a real name beats an
|
|
259
|
+
// unnamed ordinal inside shell32, which is sound and is still why nothing
|
|
260
|
+
// else reaches into the shell. What it missed is what the file contains:
|
|
261
|
+
// on Windows 11 aero_link.cur is "Link Select", the pointing hand -- the
|
|
262
|
+
// same cursor `pointer` already gets from IDC_HAND. So `alias` and
|
|
263
|
+
// `pointer` came back pixel for pixel identical, and CSS means two
|
|
264
|
+
// different things by them.
|
|
265
|
+
//
|
|
266
|
+
// The art below is the shape `alias` is supposed to be: an arrow with the
|
|
267
|
+
// shortcut badge turning up and to the right. It was already written and
|
|
268
|
+
// never ran, because the file always loaded.
|
|
273
269
|
return cursorFromArt(aliasArt(), 0, 0);
|
|
274
270
|
}
|
|
275
271
|
if (keyword == "cell") {
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
#include "PlatformServices.h"
|
|
16
16
|
#include "UIManagerAccess.h"
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// Either Skia: the 2.x peer in this package or the 3.x one in basalt-skia,
|
|
19
|
+
// which share this header's two functions and are never both linked. See
|
|
20
|
+
// Win32SkiaPeer.h.
|
|
21
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
19
22
|
#include "Win32SkiaPeer.h"
|
|
20
23
|
#endif
|
|
21
24
|
|
|
@@ -343,7 +346,7 @@ void Win32MountingManager::forgetTag(Tag tag) {
|
|
|
343
346
|
labels_.forget(tag);
|
|
344
347
|
liveRegions_.forget(tag);
|
|
345
348
|
liveRegionLabels_.erase(tag);
|
|
346
|
-
#
|
|
349
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
347
350
|
// Unregisters the canvas. This is the one moment a view is known to be
|
|
348
351
|
// finished with, and a canvas left registered is a surface the package will
|
|
349
352
|
// keep rendering into for a tag nothing will ever draw again.
|
|
@@ -360,7 +363,7 @@ void Win32MountingManager::updateView(RnWin32View *view, const ShadowView &shado
|
|
|
360
363
|
applyText(view, shadowView);
|
|
361
364
|
applyImage(view, shadowView);
|
|
362
365
|
applyAccessibility(view, shadowView);
|
|
363
|
-
#
|
|
366
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
364
367
|
// After applyProps, which is what puts nativeID on the view -- a canvas
|
|
365
368
|
// registers itself by that id and cannot attach before it is there. The size
|
|
366
369
|
// it reads comes from the shadow view rather than from the frame
|