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
|
@@ -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
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#include "Win32TextInput.h"
|
|
2
2
|
|
|
3
3
|
#include "DirectWriteLayout.h"
|
|
4
|
+
#include "DragAndDrop.h"
|
|
4
5
|
#include "TextChecking.h"
|
|
6
|
+
#include "Win32PeerClip.h"
|
|
5
7
|
#include "Win32InputScopes.h"
|
|
6
8
|
#include "Win32Strings.h"
|
|
7
9
|
|
|
@@ -64,61 +66,37 @@ COLORREF toColorRef(const float components[4]) {
|
|
|
64
66
|
return RGB(channel(components[0]), channel(components[1]), channel(components[2]));
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
+
// Where a peer is placed and what of it is still the peer's: see
|
|
70
|
+
// win32/Win32PeerClip.h, which is the whole of the geometry. It lives there
|
|
71
|
+
// rather than here so that it can be tested against a view tree with no window
|
|
72
|
+
// and no React Native, the same split Win32TitleBarLayout made.
|
|
69
73
|
//
|
|
70
74
|
// Translation only: an EDIT is a window and a window cannot be rotated, so a
|
|
71
75
|
// transformed field's peer sits where the untransformed one would. That is a
|
|
72
76
|
// real difference from the painted view behind it, and there is no cheap fix --
|
|
73
77
|
// see the header.
|
|
74
|
-
//
|
|
75
|
-
// Visibility is the other half. A child window is not clipped by anything in
|
|
76
|
-
// the React tree, so a field scrolled out of its list would otherwise go on
|
|
77
|
-
// being drawn over the list's neighbours. Any clipping ancestor that the
|
|
78
|
-
// rectangle has left entirely hides it.
|
|
79
|
-
bool peerRect(RnWin32View *view, RnWin32View *root, RECT &out) {
|
|
80
|
-
if (view == nullptr || root == nullptr) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
RECT intersectRects(const RECT &a, const RECT &b) {
|
|
80
|
+
return RECT{std::max(a.left, b.left),
|
|
81
|
+
std::max(a.top, b.top),
|
|
82
|
+
std::min(a.right, b.right),
|
|
83
|
+
std::min(a.bottom, b.bottom)};
|
|
84
|
+
}
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (current == root) {
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
x += current->frame().x;
|
|
97
|
-
y += current->frame().y;
|
|
86
|
+
RECT offsetBy(const RECT &rect, LONG dx, LONG dy) {
|
|
87
|
+
return RECT{rect.left + dx, rect.top + dy, rect.right + dx, rect.bottom + dy};
|
|
88
|
+
}
|
|
98
89
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
90
|
+
bool sameShape(const std::vector<RECT> &a, const std::vector<RECT> &b) {
|
|
91
|
+
if (a.size() != b.size()) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
for (size_t i = 0; i < a.size(); i++) {
|
|
95
|
+
if (a[i].left != b[i].left || a[i].top != b[i].top || a[i].right != b[i].right ||
|
|
96
|
+
a[i].bottom != b[i].bottom) {
|
|
103
97
|
return false;
|
|
104
98
|
}
|
|
105
|
-
x -= parent->scrollX();
|
|
106
|
-
y -= parent->scrollY();
|
|
107
|
-
|
|
108
|
-
if (parent->clipsChildren()) {
|
|
109
|
-
// In the parent's own coordinates the box is 0..width, 0..height, and
|
|
110
|
-
// (x, y) is where this view now sits relative to it.
|
|
111
|
-
if (x + width <= 0 || y + height <= 0 || x >= parent->frame().width ||
|
|
112
|
-
y >= parent->frame().height) {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
99
|
}
|
|
117
|
-
|
|
118
|
-
out.left = static_cast<LONG>(std::lround(x));
|
|
119
|
-
out.top = static_cast<LONG>(std::lround(y));
|
|
120
|
-
out.right = out.left + static_cast<LONG>(std::lround(width));
|
|
121
|
-
out.bottom = out.top + static_cast<LONG>(std::lround(height));
|
|
122
100
|
return true;
|
|
123
101
|
}
|
|
124
102
|
|
|
@@ -157,8 +135,29 @@ void Win32TextInputManager::update(RnWin32View *view, const ShadowView &shadowVi
|
|
|
157
135
|
// that still behaves as one line. So a field that changes the prop has its
|
|
158
136
|
// peer rebuilt, which is rare enough to be worth the simplicity: React Native
|
|
159
137
|
// apps choose multiline per field rather than toggling it.
|
|
138
|
+
// `acceptDragAndDropTypes`: the kinds this field will take on a drop, which
|
|
139
|
+
// makes it a drop target in the sense core/DragAndDrop.h means. Android
|
|
140
|
+
// implements the prop as a filter over the platform's own drop handling and
|
|
141
|
+
// this is that filter: a list naming only text refuses a dragged file, so
|
|
142
|
+
// the cursor says no before the drop rather than the app rejecting it after.
|
|
143
|
+
//
|
|
144
|
+
// Cleared when the prop goes away, which is the case a field that stops
|
|
145
|
+
// accepting must not keep accepting.
|
|
146
|
+
if (view != nullptr) {
|
|
147
|
+
view->setDropAccepts(props != nullptr && props->acceptDragAndDropTypes.has_value()
|
|
148
|
+
? dropAcceptsFromTypes(*props->acceptDragAndDropTypes)
|
|
149
|
+
: DropAcceptsNone);
|
|
150
|
+
}
|
|
151
|
+
|
|
160
152
|
const bool multiline = props != nullptr && props->multiline;
|
|
161
|
-
|
|
153
|
+
// `scrollEnabled`, the other creation-time style bit. React Native documents
|
|
154
|
+
// it as multiline-only -- a single line scrolls sideways through
|
|
155
|
+
// `ES_AUTOHSCROLL` and the prop says nothing about that -- so it is only
|
|
156
|
+
// compared for a multiline field, and a single-line one that sets it does
|
|
157
|
+
// not get its peer rebuilt for nothing.
|
|
158
|
+
const bool scrollEnabled = props == nullptr || props->traits.scrollEnabled;
|
|
159
|
+
const bool scrollChanged = multiline && scrollEnabled != entry.scrollEnabled;
|
|
160
|
+
if (entry.control != nullptr && (multiline != entry.multiline || scrollChanged)) {
|
|
162
161
|
const bool hadFocus = GetFocus() == entry.control;
|
|
163
162
|
destroyPeer(entry);
|
|
164
163
|
entry.view = view;
|
|
@@ -184,9 +183,18 @@ void Win32TextInputManager::update(RnWin32View *view, const ShadowView &shadowVi
|
|
|
184
183
|
// scroll is what lets the caret leave the bottom of the box. No `WS_VSCROLL`
|
|
185
184
|
// on purpose -- a scrollbar inside a styled field is a Win32 scrollbar
|
|
186
185
|
// drawn over a React Native background, and neither other host shows one.
|
|
186
|
+
//
|
|
187
|
+
// `scrollEnabled: false` is that vertical scroll withheld, which is what
|
|
188
|
+
// the prop asks for: the field stops following the caret past the bottom
|
|
189
|
+
// of the box and the text below is simply not shown. An EDIT without
|
|
190
|
+
// `ES_AUTOVSCROLL` still accepts the text -- it is a display decision and
|
|
191
|
+
// not a limit, which is the same distinction `maxLength` is on the other
|
|
192
|
+
// side of.
|
|
187
193
|
entry.multiline = multiline;
|
|
194
|
+
entry.scrollEnabled = scrollEnabled;
|
|
188
195
|
const DWORD style = WS_CHILD | WS_CLIPSIBLINGS | ES_LEFT
|
|
189
|
-
| (multiline ? (ES_MULTILINE | ES_AUTOVSCROLL
|
|
196
|
+
| (multiline ? (ES_MULTILINE | (scrollEnabled ? ES_AUTOVSCROLL : 0u))
|
|
197
|
+
: ES_AUTOHSCROLL);
|
|
190
198
|
entry.control = CreateWindowEx(0,
|
|
191
199
|
L"EDIT",
|
|
192
200
|
L"",
|
|
@@ -596,6 +604,9 @@ void Win32TextInputManager::destroyPeer(Entry &entry) {
|
|
|
596
604
|
DestroyWindow(entry.control);
|
|
597
605
|
entry.control = nullptr;
|
|
598
606
|
}
|
|
607
|
+
// The window took the region with it, and a rebuilt peer -- which is what a
|
|
608
|
+
// field switching to multiline gets -- starts without one.
|
|
609
|
+
entry.regionShape.clear();
|
|
599
610
|
if (entry.font != nullptr) {
|
|
600
611
|
DeleteObject(entry.font);
|
|
601
612
|
entry.font = nullptr;
|
|
@@ -630,11 +641,12 @@ void Win32TextInputManager::syncBounds(RnWin32View *root) {
|
|
|
630
641
|
if (entry.control == nullptr) {
|
|
631
642
|
continue;
|
|
632
643
|
}
|
|
633
|
-
|
|
634
|
-
if (!
|
|
644
|
+
const win32::PeerClip placement = win32::peerClip(entry.view, root);
|
|
645
|
+
if (!placement.visible) {
|
|
635
646
|
ShowWindow(entry.control, SW_HIDE);
|
|
636
647
|
continue;
|
|
637
648
|
}
|
|
649
|
+
RECT rect = placement.bounds;
|
|
638
650
|
|
|
639
651
|
// Inside the content insets, so that `paddingHorizontal` on a field means
|
|
640
652
|
// what it means on a <View>. GTK allocates its GtkText inside the same
|
|
@@ -673,6 +685,74 @@ void Win32TextInputManager::syncBounds(RnWin32View *root) {
|
|
|
673
685
|
continue;
|
|
674
686
|
}
|
|
675
687
|
|
|
688
|
+
// What of the control is still the control's, in its own coordinates: the
|
|
689
|
+
// part of it inside every clipping ancestor, less everything painted over
|
|
690
|
+
// it. See Win32PeerClip.h -- this is the half that turns those rectangles
|
|
691
|
+
// into the one thing USER32 understands, which is a window region.
|
|
692
|
+
//
|
|
693
|
+
// The first piece is what is left of the control, and each one after it is
|
|
694
|
+
// taken out.
|
|
695
|
+
std::vector<RECT> shape;
|
|
696
|
+
const RECT visible = offsetBy(intersectRects(placement.clip, rect), -rect.left, -rect.top);
|
|
697
|
+
if (visible.right > visible.left && visible.bottom > visible.top) {
|
|
698
|
+
shape.push_back(visible);
|
|
699
|
+
for (const RECT &occluder : placement.occluders) {
|
|
700
|
+
const RECT over = offsetBy(intersectRects(occluder, rect), -rect.left, -rect.top);
|
|
701
|
+
if (over.right > over.left && over.bottom > over.top) {
|
|
702
|
+
shape.push_back(over);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Whole and unobstructed is the common case and gets no region at all:
|
|
708
|
+
// `SetWindowRgn` redraws the control, and this runs after every wheel
|
|
709
|
+
// notch, so a field in a list being scrolled would otherwise be told to
|
|
710
|
+
// redraw sixty times a second for a shape that has not changed.
|
|
711
|
+
HRGN region = nullptr;
|
|
712
|
+
const bool whole = shape.size() == 1 && shape[0].left == 0 && shape[0].top == 0 &&
|
|
713
|
+
shape[0].right == rect.right - rect.left && shape[0].bottom == rect.bottom - rect.top;
|
|
714
|
+
if (whole) {
|
|
715
|
+
shape.clear();
|
|
716
|
+
} else if (!shape.empty()) {
|
|
717
|
+
// The system owns the region once it is handed over, so the pieces taken
|
|
718
|
+
// out of it are deleted here and the result is not.
|
|
719
|
+
region = CreateRectRgn(shape[0].left, shape[0].top, shape[0].right, shape[0].bottom);
|
|
720
|
+
for (size_t i = 1; i < shape.size(); i++) {
|
|
721
|
+
HRGN piece = CreateRectRgn(shape[i].left, shape[i].top, shape[i].right, shape[i].bottom);
|
|
722
|
+
CombineRgn(region, region, piece, RGN_DIFF);
|
|
723
|
+
DeleteObject(piece);
|
|
724
|
+
}
|
|
725
|
+
RECT box{};
|
|
726
|
+
if (GetRgnBox(region, &box) == NULLREGION) {
|
|
727
|
+
// The pieces swallowed it: the field is wholly behind something rather
|
|
728
|
+
// than partly, which is what a modal over a pane looks like.
|
|
729
|
+
DeleteObject(region);
|
|
730
|
+
region = nullptr;
|
|
731
|
+
shape.clear();
|
|
732
|
+
ShowWindow(entry.control, SW_HIDE);
|
|
733
|
+
entry.regionShape.clear();
|
|
734
|
+
SetWindowRgn(entry.control, nullptr, FALSE);
|
|
735
|
+
// And it does not keep the keyboard. A field left focused behind a
|
|
736
|
+
// modal swallows every key the modal wanted, Escape included, so the
|
|
737
|
+
// sheet that covered it cannot be closed -- the same bug as the field
|
|
738
|
+
// being visible over the modal, one layer further in. The question does
|
|
739
|
+
// not arise on the other two desktops: a field there is a widget, and a
|
|
740
|
+
// widget that is not drawn is not in the focus chain.
|
|
741
|
+
if (GetFocus() == entry.control) {
|
|
742
|
+
moveFocusToHost();
|
|
743
|
+
}
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
} else {
|
|
747
|
+
// Out of sight behind a clipping ancestor, which `placement.visible`
|
|
748
|
+
// catches when it is complete and this catches when the control's own
|
|
749
|
+
// strip is what fell outside.
|
|
750
|
+
ShowWindow(entry.control, SW_HIDE);
|
|
751
|
+
entry.regionShape.clear();
|
|
752
|
+
SetWindowRgn(entry.control, nullptr, FALSE);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
|
|
676
756
|
SetWindowPos(entry.control,
|
|
677
757
|
nullptr,
|
|
678
758
|
rect.left,
|
|
@@ -680,6 +760,17 @@ void Win32TextInputManager::syncBounds(RnWin32View *root) {
|
|
|
680
760
|
rect.right - rect.left,
|
|
681
761
|
rect.bottom - rect.top,
|
|
682
762
|
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
763
|
+
|
|
764
|
+
if (sameShape(shape, entry.regionShape)) {
|
|
765
|
+
if (region != nullptr) {
|
|
766
|
+
DeleteObject(region);
|
|
767
|
+
}
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
entry.regionShape = shape;
|
|
771
|
+
// A null region removes the one the control had, which is what a field
|
|
772
|
+
// that has come back into the clear needs.
|
|
773
|
+
SetWindowRgn(entry.control, region, TRUE);
|
|
683
774
|
}
|
|
684
775
|
}
|
|
685
776
|
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
// mutation arrives at all. `syncBounds` is that, and the host calls it after
|
|
30
30
|
// every transaction and every wheel.
|
|
31
31
|
//
|
|
32
|
-
// - **It
|
|
33
|
-
// in an order the React tree has no say in
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
32
|
+
// - **It would be on top of everything.** Child windows paint above whatever
|
|
33
|
+
// Direct2D drew, in an order the React tree has no say in, so a field
|
|
34
|
+
// neither clips to a scrolled ancestor nor can be covered by a later
|
|
35
|
+
// sibling. `SetWindowRgn` is where the missing order goes, and working the
|
|
36
|
+
// region out from the tree is `Win32PeerClip.h`; `syncBounds` installs it.
|
|
37
|
+
// What is still not possible is a transform, because a window cannot be
|
|
38
|
+
// rotated -- see docs/backlog/platform-windows.md.
|
|
37
39
|
//
|
|
38
40
|
// - **Its events arrive at the parent.** EN_CHANGE, EN_SETFOCUS and
|
|
39
41
|
// EN_KILLFOCUS are WM_COMMAND notifications to the host window, and the
|
|
@@ -110,9 +112,11 @@ class Win32TextInputManager {
|
|
|
110
112
|
const std::string &name,
|
|
111
113
|
const folly::dynamic &args);
|
|
112
114
|
|
|
113
|
-
// Moves every peer to where its view now is, in client coordinates,
|
|
114
|
-
// the ones that have gone out of
|
|
115
|
-
//
|
|
115
|
+
// Moves every peer to where its view now is, in client coordinates, clips it
|
|
116
|
+
// to what the tree leaves of it, and hides the ones that have gone out of
|
|
117
|
+
// sight or behind something. Called after each transaction and each scroll:
|
|
118
|
+
// a child window does not follow a view that has no widget, and the region
|
|
119
|
+
// it is clipped to changes with every one of them. See Win32PeerClip.h.
|
|
116
120
|
void syncBounds(win32::RnWin32View *root);
|
|
117
121
|
|
|
118
122
|
// WM_COMMAND from the host: EN_CHANGE, EN_SETFOCUS, EN_KILLFOCUS. True when
|
|
@@ -260,6 +264,11 @@ class Win32TextInputManager {
|
|
|
260
264
|
// created and cannot be added afterwards, so this is what a change in the
|
|
261
265
|
// prop is compared against: a field that switches has its peer rebuilt.
|
|
262
266
|
bool multiline{false};
|
|
267
|
+
// `scrollEnabled`, which is part of the same creation-time decision as
|
|
268
|
+
// `multiline`: it is the `ES_AUTOVSCROLL` style bit, read when the window
|
|
269
|
+
// is made. Remembered so a change rebuilds the peer the way a change of
|
|
270
|
+
// `multiline` does.
|
|
271
|
+
bool scrollEnabled{true};
|
|
263
272
|
|
|
264
273
|
// Yoga resolves border and padding into contentInsets, so the same twelve
|
|
265
274
|
// points mean the same thing in a field as in a <View>. Applied by placing
|
|
@@ -273,6 +282,17 @@ class Win32TextInputManager {
|
|
|
273
282
|
// is set to so that the text sits in the middle of the field.
|
|
274
283
|
LONG lineHeight{0};
|
|
275
284
|
|
|
285
|
+
// The window region last installed, as the rectangles it was built from in
|
|
286
|
+
// the control's own coordinates: the visible box first, then each piece
|
|
287
|
+
// taken out of it. Empty means no region, which is a whole unobstructed
|
|
288
|
+
// field -- the common case, and the one worth not touching.
|
|
289
|
+
//
|
|
290
|
+
// Held because `SetWindowRgn` redraws the control, and `syncBounds` runs
|
|
291
|
+
// after every transaction and every wheel notch: a field in a list being
|
|
292
|
+
// scrolled would be told to redraw on every one of them for a region that
|
|
293
|
+
// has not changed. See `syncBounds` and Win32PeerClip.h.
|
|
294
|
+
std::vector<RECT> regionShape;
|
|
295
|
+
|
|
276
296
|
// The input scope last installed for `keyboardType`, and whether one has
|
|
277
297
|
// been. `SetInputScope` replaces the window's whole scope list, so it is
|
|
278
298
|
// called on a change rather than on every props update -- a controlled
|
|
@@ -24,4 +24,15 @@ void installUiThread();
|
|
|
24
24
|
// Whether `postToUiThread` will actually marshal, rather than running inline.
|
|
25
25
|
bool hasUiThread();
|
|
26
26
|
|
|
27
|
+
// Takes it down again, and runs anything still queued on the calling thread so
|
|
28
|
+
// that nothing posted is lost. For tests, and this is the one that needs it:
|
|
29
|
+
// react-native-skia 3.x records a frame on a thread pool of its own and asks
|
|
30
|
+
// the UI thread to present it, and the producer holds a lock across that ask.
|
|
31
|
+
// Run inline -- which is what posting with no window installed means on a
|
|
32
|
+
// worker, exactly as the note above warns -- it re-enters the producer on the
|
|
33
|
+
// thread that already holds the lock, and the suite dies with no message. So
|
|
34
|
+
// the canvas test installs a real loop, pumps it, and puts things back for the
|
|
35
|
+
// tests after it, which are written against the inline behaviour.
|
|
36
|
+
void uninstallUiThread();
|
|
37
|
+
|
|
27
38
|
} // namespace basalt
|