basalt-win32 0.2.0 → 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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#include "Win32PeerClip.h"
|
|
2
|
+
|
|
3
|
+
#include "RnWin32View.h"
|
|
4
|
+
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <cmath>
|
|
7
|
+
|
|
8
|
+
namespace basalt::win32 {
|
|
9
|
+
|
|
10
|
+
namespace {
|
|
11
|
+
|
|
12
|
+
RECT boxOf(double x, double y, double width, double height) {
|
|
13
|
+
RECT rect{};
|
|
14
|
+
rect.left = static_cast<LONG>(std::lround(x));
|
|
15
|
+
rect.top = static_cast<LONG>(std::lround(y));
|
|
16
|
+
rect.right = rect.left + static_cast<LONG>(std::lround(width));
|
|
17
|
+
rect.bottom = rect.top + static_cast<LONG>(std::lround(height));
|
|
18
|
+
return rect;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
bool isEmpty(const RECT &rect) {
|
|
22
|
+
return rect.right <= rect.left || rect.bottom <= rect.top;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
RECT intersect(const RECT &a, const RECT &b) {
|
|
26
|
+
RECT out{};
|
|
27
|
+
out.left = std::max(a.left, b.left);
|
|
28
|
+
out.top = std::max(a.top, b.top);
|
|
29
|
+
out.right = std::min(a.right, b.right);
|
|
30
|
+
out.bottom = std::min(a.bottom, b.bottom);
|
|
31
|
+
if (isEmpty(out)) {
|
|
32
|
+
return RECT{0, 0, 0, 0};
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Whether this view puts something over the whole of its own box.
|
|
38
|
+
//
|
|
39
|
+
// A background colour with any alpha at all, and nothing else -- see the
|
|
40
|
+
// header for why the bar is "not transparent" rather than "opaque", and why a
|
|
41
|
+
// transformed view is left alone instead of being approximated by its bounding
|
|
42
|
+
// box.
|
|
43
|
+
bool covers(const RnWin32View *view) {
|
|
44
|
+
if (!view->hasBackgroundColor() || view->hasTransform()) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (view->opacity() <= 0.0f) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return view->backgroundColor()[3] > 0.0f;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
struct Walk {
|
|
54
|
+
const RnWin32View *target{nullptr};
|
|
55
|
+
PeerClip *out{nullptr};
|
|
56
|
+
// Set when the target has been reached, which is what divides the tree into
|
|
57
|
+
// "painted before it", which cannot cover it, and "painted after it", which
|
|
58
|
+
// can. One flag is enough because painting order is a single sequence.
|
|
59
|
+
bool seen{false};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// `origin` is where this view's parent puts its children, already scrolled.
|
|
63
|
+
void visit(RnWin32View *view, double originX, double originY, const RECT &clip, Walk &walk) {
|
|
64
|
+
if (view == nullptr || view->hidden()) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const double x = originX + view->frame().x;
|
|
69
|
+
const double y = originY + view->frame().y;
|
|
70
|
+
const RECT box = boxOf(x, y, view->frame().width, view->frame().height);
|
|
71
|
+
|
|
72
|
+
if (view == walk.target) {
|
|
73
|
+
const RECT visible = intersect(box, clip);
|
|
74
|
+
if (isEmpty(visible)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
walk.out->visible = true;
|
|
78
|
+
walk.out->bounds = box;
|
|
79
|
+
walk.out->clip = visible;
|
|
80
|
+
// Its own subtree is not walked: a child of a `<TextInput>` is inside the
|
|
81
|
+
// field rather than over it, and the field has none in any real tree.
|
|
82
|
+
walk.seen = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (walk.seen && covers(view)) {
|
|
87
|
+
const RECT over = intersect(box, clip);
|
|
88
|
+
if (!isEmpty(over)) {
|
|
89
|
+
walk.out->occluders.push_back(over);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
RECT childClip = clip;
|
|
94
|
+
if (view->clipsChildren()) {
|
|
95
|
+
childClip = intersect(clip, box);
|
|
96
|
+
if (isEmpty(childClip)) {
|
|
97
|
+
// Nothing inside can be seen, and nothing inside can cover anything
|
|
98
|
+
// either -- but the target may still be in there, and if it is then it is
|
|
99
|
+
// off screen, which is what not finding it already means.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const double childX = x - view->scrollX();
|
|
105
|
+
const double childY = y - view->scrollY();
|
|
106
|
+
for (RnWin32View *child : view->childrenInPaintOrder()) {
|
|
107
|
+
visit(child, childX, childY, childClip, walk);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} // namespace
|
|
112
|
+
|
|
113
|
+
PeerClip peerClip(RnWin32View *view, RnWin32View *root) {
|
|
114
|
+
PeerClip result{};
|
|
115
|
+
if (view == nullptr || root == nullptr) {
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The root's own frame is not part of these coordinates: the surface root is
|
|
120
|
+
// the client area, and a peer's position is measured from its top-left. So
|
|
121
|
+
// the walk starts inside it rather than at it, which also means a `view` that
|
|
122
|
+
// *is* the root is not a field and comes back invisible.
|
|
123
|
+
if (root->hidden()) {
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Unbounded unless the root says otherwise, which is the rule every other
|
|
128
|
+
// ancestor is under. Clipping to the client area regardless would be defen-
|
|
129
|
+
// sible and is not done here: a surface that has never been laid out has a
|
|
130
|
+
// zero frame, and "every field is invisible" is a worse answer than "a field
|
|
131
|
+
// outside the window is placed outside the window", which USER32 handles.
|
|
132
|
+
constexpr LONG kUnbounded = 1 << 28;
|
|
133
|
+
RECT clip{-kUnbounded, -kUnbounded, kUnbounded, kUnbounded};
|
|
134
|
+
if (root->clipsChildren()) {
|
|
135
|
+
clip = boxOf(0.0, 0.0, root->frame().width, root->frame().height);
|
|
136
|
+
if (isEmpty(clip)) {
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
Walk walk{view, &result, false};
|
|
141
|
+
const double childX = -static_cast<double>(root->scrollX());
|
|
142
|
+
const double childY = -static_cast<double>(root->scrollY());
|
|
143
|
+
for (RnWin32View *child : root->childrenInPaintOrder()) {
|
|
144
|
+
visit(child, childX, childY, clip, walk);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!result.visible) {
|
|
148
|
+
return PeerClip{};
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
} // namespace basalt::win32
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Where a child-window peer is allowed to paint.
|
|
2
|
+
//
|
|
3
|
+
// A `<TextInput>` on this host is a real EDIT control, so it is a child window
|
|
4
|
+
// rather than something Direct2D draws. That buys the IME, the clipboard and
|
|
5
|
+
// every binding a Windows user expects, and it costs the one thing every other
|
|
6
|
+
// view gets for free: a place in the painting order. A child window is drawn by
|
|
7
|
+
// USER32 after the parent's client area, so it sits above everything the React
|
|
8
|
+
// tree drew, whatever the tree says. Two things follow, and a person sees both:
|
|
9
|
+
//
|
|
10
|
+
// - A field in a `<ScrollView>` does not clip to it. Scroll the list until
|
|
11
|
+
// the field is half out and the half that should be gone goes on being
|
|
12
|
+
// drawn over the list's neighbour.
|
|
13
|
+
// - A field cannot be covered. Open a modal over a pane that has one and the
|
|
14
|
+
// field floats on top of the modal, at full brightness, still editable.
|
|
15
|
+
//
|
|
16
|
+
// Both are the same missing fact -- which pixels of the field are still the
|
|
17
|
+
// field's -- and Windows has somewhere to put it: `SetWindowRgn`. A window with
|
|
18
|
+
// a region is displayed only inside it, and clicks outside it fall through to
|
|
19
|
+
// the parent. So the job is to work the region out from the view tree, which is
|
|
20
|
+
// what this file is: pure geometry over `RnWin32View`, with no window, no
|
|
21
|
+
// control and no React Native in it, so that it can be tested with none of
|
|
22
|
+
// them.
|
|
23
|
+
//
|
|
24
|
+
// ## What counts as covering it
|
|
25
|
+
//
|
|
26
|
+
// Clipping is exact: a clipping ancestor's box is a rectangle and the
|
|
27
|
+
// intersection of rectangles is the answer.
|
|
28
|
+
//
|
|
29
|
+
// Occlusion is not, and cannot be. The right behaviour for a field under
|
|
30
|
+
// kino's modal scrim -- `#000000A0` over the whole window -- is for the field
|
|
31
|
+
// to be *dimmed*, and a child window cannot be dimmed by something painted into
|
|
32
|
+
// its parent: the two are composited by USER32, in that order, with no blend to
|
|
33
|
+
// take part in. The choice is therefore between showing the field undimmed and
|
|
34
|
+
// hiding it, and hiding it is the better wrong answer: an undimmed field over a
|
|
35
|
+
// modal is the bug people report, while a field that goes quiet under a scrim
|
|
36
|
+
// reads as part of the modal.
|
|
37
|
+
//
|
|
38
|
+
// So anything painted after the field that is not fully transparent counts as
|
|
39
|
+
// covering it. That is deliberately more eager than "opaque": a 63%-black
|
|
40
|
+
// backdrop blends to something no child window can match, so there is nothing
|
|
41
|
+
// to be gained by letting it show through at 100%.
|
|
42
|
+
//
|
|
43
|
+
// What does *not* count: a view with no background at all, which is every
|
|
44
|
+
// layout wrapper in a real tree and the reason this rule produces no false
|
|
45
|
+
// positives in practice, and a transformed view, whose box is not a rectangle
|
|
46
|
+
// in these coordinates and which is therefore left alone rather than guessed
|
|
47
|
+
// at. A `<Text>` drawn over a field is also left alone -- glyphs are not a box.
|
|
48
|
+
//
|
|
49
|
+
// ## Transforms
|
|
50
|
+
//
|
|
51
|
+
// Ignored throughout, as `Win32TextInputManager` already ignores them when it
|
|
52
|
+
// places the peer: a window cannot be rotated, so a transformed field's peer
|
|
53
|
+
// sits where the untransformed one would, and clipping it to a rotated
|
|
54
|
+
// ancestor's bounding box would only move the error around.
|
|
55
|
+
|
|
56
|
+
#pragma once
|
|
57
|
+
|
|
58
|
+
#include <windows.h>
|
|
59
|
+
|
|
60
|
+
#include <vector>
|
|
61
|
+
|
|
62
|
+
namespace basalt::win32 {
|
|
63
|
+
|
|
64
|
+
class RnWin32View;
|
|
65
|
+
|
|
66
|
+
// Where `view`'s peer may paint, all in the surface root's coordinates.
|
|
67
|
+
struct PeerClip {
|
|
68
|
+
// False when the view is not on screen at all: hidden, detached, or scrolled
|
|
69
|
+
// entirely out of a clipping ancestor. Nothing else in the struct means
|
|
70
|
+
// anything then.
|
|
71
|
+
bool visible{false};
|
|
72
|
+
|
|
73
|
+
// The view's own box, unclipped. This is where the control goes -- a control
|
|
74
|
+
// narrowed to its visible part would lay its text out in the wrong width.
|
|
75
|
+
RECT bounds{};
|
|
76
|
+
|
|
77
|
+
// `bounds` narrowed by every clipping ancestor. Never empty when `visible`.
|
|
78
|
+
RECT clip{};
|
|
79
|
+
|
|
80
|
+
// Boxes painted after the view that cover part of it, already narrowed to
|
|
81
|
+
// their own clipping ancestors. Each is taken out of the region.
|
|
82
|
+
std::vector<RECT> occluders{};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Walks the tree from `root` once, in painting order, and answers for `view`.
|
|
86
|
+
//
|
|
87
|
+
// `root` is the surface root, whose coordinates are the window's client
|
|
88
|
+
// coordinates on this host. A view that is not under `root` comes back
|
|
89
|
+
// invisible, which is what a peer between a Remove and its Delete looks like.
|
|
90
|
+
PeerClip peerClip(RnWin32View *view, RnWin32View *root);
|
|
91
|
+
|
|
92
|
+
} // namespace basalt::win32
|
|
@@ -4,28 +4,49 @@
|
|
|
4
4
|
|
|
5
5
|
#include <react/utils/RunLoopObserverManager.h>
|
|
6
6
|
|
|
7
|
+
#include <chrono>
|
|
8
|
+
|
|
7
9
|
namespace basalt {
|
|
8
10
|
|
|
9
|
-
int
|
|
10
|
-
std::shared_ptr<facebook::react::RunLoopObserverManager> manager) {
|
|
11
|
+
int runMessageLoop(const std::function<void()> &beat) {
|
|
11
12
|
MSG message{};
|
|
13
|
+
|
|
12
14
|
for (;;) {
|
|
13
|
-
// Drain
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// Drain what is queued before beating. Beating between each message would
|
|
16
|
+
// be correct but wasteful, and beating *after* the queue is empty is what
|
|
17
|
+
// "BeforeWaiting" means -- the frame has settled, and whatever the messages
|
|
18
|
+
// produced is now worth flushing to JavaScript in one go.
|
|
19
|
+
//
|
|
20
|
+
// Bounded, though: "after the queue is empty" is a moment that never comes
|
|
21
|
+
// in an app that paints continuously, and an event waiting for it waits
|
|
22
|
+
// forever. See the header for what that cost -- a button that usually
|
|
23
|
+
// worked.
|
|
24
|
+
auto since = std::chrono::steady_clock::now();
|
|
25
|
+
int drained = 0;
|
|
17
26
|
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
|
18
27
|
if (message.message == WM_QUIT) {
|
|
19
28
|
return static_cast<int>(message.wParam);
|
|
20
29
|
}
|
|
21
30
|
TranslateMessage(&message);
|
|
22
31
|
DispatchMessageW(&message);
|
|
23
|
-
}
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
// The count is checked first because it is free, and the clock only
|
|
34
|
+
// every `kBeatEveryMessages` messages, so a busy drain does not spend
|
|
35
|
+
// itself in `steady_clock::now`.
|
|
36
|
+
if (++drained < kBeatEveryMessages) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
drained = 0;
|
|
40
|
+
const auto now = std::chrono::steady_clock::now();
|
|
41
|
+
if (now - since < std::chrono::milliseconds(kBeatEveryMs)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
since = now;
|
|
45
|
+
beat();
|
|
27
46
|
}
|
|
28
47
|
|
|
48
|
+
beat();
|
|
49
|
+
|
|
29
50
|
// Block until something arrives. MsgWaitForMultipleObjectsEx rather than
|
|
30
51
|
// GetMessage, because the beat above may itself have posted work -- a mount
|
|
31
52
|
// marshalled from the JS thread arrives as a posted message -- and
|
|
@@ -45,4 +66,13 @@ int runMessageLoopWithBeat(
|
|
|
45
66
|
}
|
|
46
67
|
}
|
|
47
68
|
|
|
69
|
+
int runMessageLoopWithBeat(
|
|
70
|
+
std::shared_ptr<facebook::react::RunLoopObserverManager> manager) {
|
|
71
|
+
return runMessageLoop([&manager] {
|
|
72
|
+
if (manager != nullptr) {
|
|
73
|
+
manager->onRender();
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
} // namespace basalt
|
|
@@ -24,9 +24,32 @@
|
|
|
24
24
|
//
|
|
25
25
|
// So this is a loop rather than an observer, and the host calls it instead of
|
|
26
26
|
// writing its own.
|
|
27
|
+
//
|
|
28
|
+
// ## "Until it comes up empty" is not enough on its own
|
|
29
|
+
//
|
|
30
|
+
// That rule assumes the queue eventually comes up empty, and in a real app it
|
|
31
|
+
// does not. kino's editor, whose preview is a Skia `<Canvas>`, posts work from
|
|
32
|
+
// the JavaScript thread around 250 times a second and invalidates its window
|
|
33
|
+
// around 120 -- so `PeekMessage` keeps answering, the drain keeps draining,
|
|
34
|
+
// and the beat below it waits for a gap that never comes. The queue is a
|
|
35
|
+
// treadmill and the beat is at the end of it.
|
|
36
|
+
//
|
|
37
|
+
// What that looks like from outside is the worst kind of bug: a button that
|
|
38
|
+
// usually works. The touch was hit-tested, the emitter was found, the event
|
|
39
|
+
// was enqueued -- and then sat in the queue until some accident of timing let
|
|
40
|
+
// the loop reach the bottom. Measured on that app, one scripted tap in three
|
|
41
|
+
// did nothing at all, and a real click, which lands later in a run and under
|
|
42
|
+
// more load, did nothing far more often than that.
|
|
43
|
+
//
|
|
44
|
+
// So the drain is bounded: the beat also happens every `kBeatEveryMs` and
|
|
45
|
+
// every `kBeatEveryMessages`, whichever comes first. Neither number is a
|
|
46
|
+
// tuning parameter to get right -- they are an upper bound on how long an
|
|
47
|
+
// event may sit, and the cost of beating early is one pass over an empty
|
|
48
|
+
// queue.
|
|
27
49
|
|
|
28
50
|
#pragma once
|
|
29
51
|
|
|
52
|
+
#include <functional>
|
|
30
53
|
#include <memory>
|
|
31
54
|
|
|
32
55
|
namespace facebook::react {
|
|
@@ -35,8 +58,24 @@ class RunLoopObserverManager;
|
|
|
35
58
|
|
|
36
59
|
namespace basalt {
|
|
37
60
|
|
|
38
|
-
//
|
|
39
|
-
// the
|
|
61
|
+
// How long an event may wait, and how many messages may go by, before the
|
|
62
|
+
// beat happens whether or not the queue has emptied. Eight milliseconds is
|
|
63
|
+
// half a frame at sixty hertz: an event enqueued at the worst moment still
|
|
64
|
+
// reaches JavaScript inside the frame it belongs to.
|
|
65
|
+
inline constexpr unsigned kBeatEveryMs = 8;
|
|
66
|
+
inline constexpr int kBeatEveryMessages = 64;
|
|
67
|
+
|
|
68
|
+
// Runs until WM_QUIT, calling `beat` each time the queue empties -- and, on a
|
|
69
|
+
// queue that does not empty, at least every `kBeatEveryMs`. Returns the exit
|
|
70
|
+
// code from WM_QUIT.
|
|
71
|
+
//
|
|
72
|
+
// The beat is a parameter rather than the manager itself because the bound
|
|
73
|
+
// above is the whole point of this file and a test needs to be able to count
|
|
74
|
+
// beats. `beat` may post WM_QUIT, which is how a test ends the loop.
|
|
75
|
+
int runMessageLoop(const std::function<void()> &beat);
|
|
76
|
+
|
|
77
|
+
// The host's own entry point: the loop above, beating React Native's event
|
|
78
|
+
// queue. A null manager runs the loop with nothing to beat.
|
|
40
79
|
int runMessageLoopWithBeat(std::shared_ptr<facebook::react::RunLoopObserverManager> manager);
|
|
41
80
|
|
|
42
81
|
} // namespace basalt
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include <utility>
|
|
7
7
|
|
|
8
8
|
#include "PlatformServices.h"
|
|
9
|
+
#include "Win32UiTimer.h"
|
|
9
10
|
|
|
10
11
|
#include <react/renderer/components/scrollview/ScrollEvent.h>
|
|
11
12
|
#include <react/renderer/components/scrollview/ScrollViewEventEmitter.h>
|
|
@@ -437,34 +438,6 @@ void Win32ScrollViewManager::endWheelDrag(Tag tag, std::uint64_t generation) {
|
|
|
437
438
|
// Offset, events and state
|
|
438
439
|
// ---------------------------------------------------------------------------
|
|
439
440
|
|
|
440
|
-
namespace {
|
|
441
|
-
|
|
442
|
-
// Which manager and view a timer is animating. A TIMERPROC carries no state, so
|
|
443
|
-
// the id is the key -- and the map is the reason `stopAnimation` must run before
|
|
444
|
-
// an entry goes, exactly as the display link does on AppKit.
|
|
445
|
-
std::unordered_map<UINT_PTR, std::pair<Win32ScrollViewManager *, facebook::react::Tag>> &
|
|
446
|
-
animations() {
|
|
447
|
-
static std::unordered_map<UINT_PTR, std::pair<Win32ScrollViewManager *, facebook::react::Tag>>
|
|
448
|
-
value;
|
|
449
|
-
return value;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
} // namespace
|
|
453
|
-
|
|
454
|
-
void CALLBACK Win32ScrollViewManager::onAnimationTimer(HWND /*hwnd*/,
|
|
455
|
-
UINT /*message*/,
|
|
456
|
-
UINT_PTR id,
|
|
457
|
-
DWORD now) {
|
|
458
|
-
const auto it = animations().find(id);
|
|
459
|
-
if (it == animations().end()) {
|
|
460
|
-
KillTimer(nullptr, id);
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
Win32ScrollViewManager *manager = it->second.first;
|
|
464
|
-
const facebook::react::Tag tag = it->second.second;
|
|
465
|
-
manager->advanceAnimation(tag, static_cast<double>(now) / 1000.0);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
441
|
void Win32ScrollViewManager::scrollTowards(Entry &entry, double x, double y, bool animated) {
|
|
469
442
|
stopAnimation(entry);
|
|
470
443
|
if (!animated || !entry.animation.start(entry.offsetX, entry.offsetY, x, y)) {
|
|
@@ -474,14 +447,21 @@ void Win32ScrollViewManager::scrollTowards(Entry &entry, double x, double y, boo
|
|
|
474
447
|
// Sixteen milliseconds: this host's choreographer runs on a timer of the same
|
|
475
448
|
// period, and matching it keeps a scroll and an animation stepping together
|
|
476
449
|
// rather than beating against each other.
|
|
477
|
-
|
|
450
|
+
//
|
|
451
|
+
// The callback carries the tag rather than being looked up from a timer id,
|
|
452
|
+
// which is what a TIMERPROC forced and what the map here used to be for.
|
|
453
|
+
const facebook::react::Tag tag = entry.tag;
|
|
454
|
+
entry.animationTimer = scheduleUiTimer(16, true, [this, tag] {
|
|
455
|
+
advanceAnimation(tag, static_cast<double>(GetTickCount64()) / 1000.0);
|
|
456
|
+
});
|
|
478
457
|
if (entry.animationTimer == 0) {
|
|
458
|
+
// No loop to animate on -- the unit tests, which install none. Land on the
|
|
459
|
+
// target rather than sit half-way to it forever.
|
|
479
460
|
entry.animation.stop();
|
|
480
461
|
applyOffset(entry, x, y, true);
|
|
481
462
|
return;
|
|
482
463
|
}
|
|
483
464
|
entry.animationLastMillis = 0;
|
|
484
|
-
animations()[entry.animationTimer] = {this, entry.tag};
|
|
485
465
|
}
|
|
486
466
|
|
|
487
467
|
bool Win32ScrollViewManager::settleOnSnapPoint(Entry &entry, double velocityY) {
|
|
@@ -500,8 +480,9 @@ bool Win32ScrollViewManager::settleOnSnapPoint(Entry &entry, double velocityY) {
|
|
|
500
480
|
void Win32ScrollViewManager::stopAnimation(Entry &entry) {
|
|
501
481
|
entry.animation.stop();
|
|
502
482
|
if (entry.animationTimer != 0) {
|
|
503
|
-
|
|
504
|
-
|
|
483
|
+
// Called from inside the animation's own callback when the scroll
|
|
484
|
+
// finishes, which `cancelUiTimer` is written to accept.
|
|
485
|
+
cancelUiTimer(entry.animationTimer);
|
|
505
486
|
entry.animationTimer = 0;
|
|
506
487
|
}
|
|
507
488
|
}
|
|
@@ -153,11 +153,13 @@ class Win32ScrollViewManager {
|
|
|
153
153
|
|
|
154
154
|
// An animated `scrollTo`. Windows models no fling -- a wheel supplies no
|
|
155
155
|
// velocity -- but an animated scroll is given its target, so the argument
|
|
156
|
-
// that rules out momentum does not rule this out. Driven by a
|
|
157
|
-
//
|
|
156
|
+
// that rules out momentum does not rule this out. Driven by a
|
|
157
|
+
// win32/Win32UiTimer.h timer rather than by `SetTimer`, for the reason
|
|
158
|
+
// that header gives: a WM_TIMER at sixty hertz stops arriving in an app
|
|
159
|
+
// that repaints continuously, which is to say mid-scroll.
|
|
158
160
|
ScrollSnapConfig snap;
|
|
159
161
|
ScrollAnimation animation;
|
|
160
|
-
|
|
162
|
+
std::uint64_t animationTimer{0};
|
|
161
163
|
unsigned long long animationLastMillis{0};
|
|
162
164
|
|
|
163
165
|
std::shared_ptr<const facebook::react::ScrollViewShadowNode::ConcreteState> state;
|
|
@@ -236,9 +238,6 @@ class Win32ScrollViewManager {
|
|
|
236
238
|
void stopAnimation(Entry &entry);
|
|
237
239
|
void advanceAnimation(facebook::react::Tag tag, double seconds);
|
|
238
240
|
|
|
239
|
-
// The timer procedure cannot carry a pointer, so it looks the animation up by
|
|
240
|
-
// timer id. Static because `SetTimer` calls a plain function.
|
|
241
|
-
static void CALLBACK onAnimationTimer(HWND hwnd, UINT message, UINT_PTR id, DWORD now);
|
|
242
241
|
void emitScrollEvent(Entry &entry, const char *which);
|
|
243
242
|
void writeStateOffset(const Entry &entry);
|
|
244
243
|
|
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
// The Win32 half of a `<Canvas>`: a
|
|
1
|
+
// The Win32 half of a `<Canvas>`: a surface drawn in the paint walk.
|
|
2
|
+
//
|
|
3
|
+
// ## Two implementations, one pair of functions
|
|
4
|
+
//
|
|
5
|
+
// Both generations of react-native-skia answer to the two functions at the
|
|
6
|
+
// bottom of this file, and they are never both linked: the 2.x one is
|
|
7
|
+
// `Win32SkiaPeer.cpp` beside this header, compiled when core's cmake/Skia.cmake
|
|
8
|
+
// found a 2.x install, and the 3.x one is basalt-skia's
|
|
9
|
+
// `win32/BasaltSkiaCanvasWin32.cpp`, compiled when it found a 3.x one.
|
|
10
|
+
// cmake/Skia.cmake refuses a 3.x install by version, so exactly one definition
|
|
11
|
+
// of each exists. Win32MountingManager.cpp calls them under a guard that
|
|
12
|
+
// covers either, the way the AppKit one does -- see AppKitSkiaPeer.h, which is
|
|
13
|
+
// this arrangement on the other host.
|
|
14
|
+
//
|
|
15
|
+
// They differ in the component they answer to, which is 3.x's rename: a 2.x
|
|
16
|
+
// `<Canvas>` mounts a `SkiaPictureView` and a 3.x one a `SkiaView`. Both
|
|
17
|
+
// descriptors are registered on all three hosts.
|
|
18
|
+
//
|
|
19
|
+
// What follows describes the 2.x one. The 3.x one reaches the same place by a
|
|
20
|
+
// different route -- it renders on the GPU through Dawn and D3D12 and reads
|
|
21
|
+
// the frame back, rather than rasterising on the CPU -- and for the same
|
|
22
|
+
// reason, which is the next paragraph. BasaltSkiaCanvasWin32.h has its half.
|
|
2
23
|
//
|
|
3
24
|
// The other two hosts give the package a layer of its own -- a CAMetalLayer on
|
|
4
25
|
// macOS, a GL surface on GTK -- and let it present straight to the compositor.
|
|
@@ -39,9 +60,10 @@ namespace win32 {
|
|
|
39
60
|
class RnWin32View;
|
|
40
61
|
}
|
|
41
62
|
|
|
42
|
-
// Gives `view` a Skia canvas if `shadowView` is a
|
|
43
|
-
//
|
|
44
|
-
//
|
|
63
|
+
// Gives `view` a Skia canvas if `shadowView` is a canvas view -- a
|
|
64
|
+
// SkiaPictureView on 2.x, a SkiaView on 3.x -- updating its size if it
|
|
65
|
+
// already has one. False when the component is something else, so the caller
|
|
66
|
+
// can treat this as a filter rather than asking twice.
|
|
45
67
|
//
|
|
46
68
|
// `requestRepaint` is how a picture pushed from JavaScript reaches the screen.
|
|
47
69
|
// Windows repaints on demand, so rendering a frame into the surface changes
|