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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include "DirectWriteLayout.h"
|
|
4
4
|
#include "TextChecking.h"
|
|
5
|
+
#include "Win32PeerClip.h"
|
|
5
6
|
#include "Win32InputScopes.h"
|
|
6
7
|
#include "Win32Strings.h"
|
|
7
8
|
|
|
@@ -64,61 +65,37 @@ COLORREF toColorRef(const float components[4]) {
|
|
|
64
65
|
return RGB(channel(components[0]), channel(components[1]), channel(components[2]));
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
68
|
+
// Where a peer is placed and what of it is still the peer's: see
|
|
69
|
+
// win32/Win32PeerClip.h, which is the whole of the geometry. It lives there
|
|
70
|
+
// rather than here so that it can be tested against a view tree with no window
|
|
71
|
+
// and no React Native, the same split Win32TitleBarLayout made.
|
|
69
72
|
//
|
|
70
73
|
// Translation only: an EDIT is a window and a window cannot be rotated, so a
|
|
71
74
|
// transformed field's peer sits where the untransformed one would. That is a
|
|
72
75
|
// real difference from the painted view behind it, and there is no cheap fix --
|
|
73
76
|
// 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
77
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
RECT intersectRects(const RECT &a, const RECT &b) {
|
|
79
|
+
return RECT{std::max(a.left, b.left),
|
|
80
|
+
std::max(a.top, b.top),
|
|
81
|
+
std::min(a.right, b.right),
|
|
82
|
+
std::min(a.bottom, b.bottom)};
|
|
83
|
+
}
|
|
88
84
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
if (current == root) {
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
x += current->frame().x;
|
|
97
|
-
y += current->frame().y;
|
|
85
|
+
RECT offsetBy(const RECT &rect, LONG dx, LONG dy) {
|
|
86
|
+
return RECT{rect.left + dx, rect.top + dy, rect.right + dx, rect.bottom + dy};
|
|
87
|
+
}
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
89
|
+
bool sameShape(const std::vector<RECT> &a, const std::vector<RECT> &b) {
|
|
90
|
+
if (a.size() != b.size()) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
for (size_t i = 0; i < a.size(); i++) {
|
|
94
|
+
if (a[i].left != b[i].left || a[i].top != b[i].top || a[i].right != b[i].right ||
|
|
95
|
+
a[i].bottom != b[i].bottom) {
|
|
103
96
|
return false;
|
|
104
97
|
}
|
|
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
98
|
}
|
|
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
99
|
return true;
|
|
123
100
|
}
|
|
124
101
|
|
|
@@ -596,6 +573,9 @@ void Win32TextInputManager::destroyPeer(Entry &entry) {
|
|
|
596
573
|
DestroyWindow(entry.control);
|
|
597
574
|
entry.control = nullptr;
|
|
598
575
|
}
|
|
576
|
+
// The window took the region with it, and a rebuilt peer -- which is what a
|
|
577
|
+
// field switching to multiline gets -- starts without one.
|
|
578
|
+
entry.regionShape.clear();
|
|
599
579
|
if (entry.font != nullptr) {
|
|
600
580
|
DeleteObject(entry.font);
|
|
601
581
|
entry.font = nullptr;
|
|
@@ -630,11 +610,12 @@ void Win32TextInputManager::syncBounds(RnWin32View *root) {
|
|
|
630
610
|
if (entry.control == nullptr) {
|
|
631
611
|
continue;
|
|
632
612
|
}
|
|
633
|
-
|
|
634
|
-
if (!
|
|
613
|
+
const win32::PeerClip placement = win32::peerClip(entry.view, root);
|
|
614
|
+
if (!placement.visible) {
|
|
635
615
|
ShowWindow(entry.control, SW_HIDE);
|
|
636
616
|
continue;
|
|
637
617
|
}
|
|
618
|
+
RECT rect = placement.bounds;
|
|
638
619
|
|
|
639
620
|
// Inside the content insets, so that `paddingHorizontal` on a field means
|
|
640
621
|
// what it means on a <View>. GTK allocates its GtkText inside the same
|
|
@@ -673,6 +654,74 @@ void Win32TextInputManager::syncBounds(RnWin32View *root) {
|
|
|
673
654
|
continue;
|
|
674
655
|
}
|
|
675
656
|
|
|
657
|
+
// What of the control is still the control's, in its own coordinates: the
|
|
658
|
+
// part of it inside every clipping ancestor, less everything painted over
|
|
659
|
+
// it. See Win32PeerClip.h -- this is the half that turns those rectangles
|
|
660
|
+
// into the one thing USER32 understands, which is a window region.
|
|
661
|
+
//
|
|
662
|
+
// The first piece is what is left of the control, and each one after it is
|
|
663
|
+
// taken out.
|
|
664
|
+
std::vector<RECT> shape;
|
|
665
|
+
const RECT visible = offsetBy(intersectRects(placement.clip, rect), -rect.left, -rect.top);
|
|
666
|
+
if (visible.right > visible.left && visible.bottom > visible.top) {
|
|
667
|
+
shape.push_back(visible);
|
|
668
|
+
for (const RECT &occluder : placement.occluders) {
|
|
669
|
+
const RECT over = offsetBy(intersectRects(occluder, rect), -rect.left, -rect.top);
|
|
670
|
+
if (over.right > over.left && over.bottom > over.top) {
|
|
671
|
+
shape.push_back(over);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Whole and unobstructed is the common case and gets no region at all:
|
|
677
|
+
// `SetWindowRgn` redraws the control, and this runs after every wheel
|
|
678
|
+
// notch, so a field in a list being scrolled would otherwise be told to
|
|
679
|
+
// redraw sixty times a second for a shape that has not changed.
|
|
680
|
+
HRGN region = nullptr;
|
|
681
|
+
const bool whole = shape.size() == 1 && shape[0].left == 0 && shape[0].top == 0 &&
|
|
682
|
+
shape[0].right == rect.right - rect.left && shape[0].bottom == rect.bottom - rect.top;
|
|
683
|
+
if (whole) {
|
|
684
|
+
shape.clear();
|
|
685
|
+
} else if (!shape.empty()) {
|
|
686
|
+
// The system owns the region once it is handed over, so the pieces taken
|
|
687
|
+
// out of it are deleted here and the result is not.
|
|
688
|
+
region = CreateRectRgn(shape[0].left, shape[0].top, shape[0].right, shape[0].bottom);
|
|
689
|
+
for (size_t i = 1; i < shape.size(); i++) {
|
|
690
|
+
HRGN piece = CreateRectRgn(shape[i].left, shape[i].top, shape[i].right, shape[i].bottom);
|
|
691
|
+
CombineRgn(region, region, piece, RGN_DIFF);
|
|
692
|
+
DeleteObject(piece);
|
|
693
|
+
}
|
|
694
|
+
RECT box{};
|
|
695
|
+
if (GetRgnBox(region, &box) == NULLREGION) {
|
|
696
|
+
// The pieces swallowed it: the field is wholly behind something rather
|
|
697
|
+
// than partly, which is what a modal over a pane looks like.
|
|
698
|
+
DeleteObject(region);
|
|
699
|
+
region = nullptr;
|
|
700
|
+
shape.clear();
|
|
701
|
+
ShowWindow(entry.control, SW_HIDE);
|
|
702
|
+
entry.regionShape.clear();
|
|
703
|
+
SetWindowRgn(entry.control, nullptr, FALSE);
|
|
704
|
+
// And it does not keep the keyboard. A field left focused behind a
|
|
705
|
+
// modal swallows every key the modal wanted, Escape included, so the
|
|
706
|
+
// sheet that covered it cannot be closed -- the same bug as the field
|
|
707
|
+
// being visible over the modal, one layer further in. The question does
|
|
708
|
+
// not arise on the other two desktops: a field there is a widget, and a
|
|
709
|
+
// widget that is not drawn is not in the focus chain.
|
|
710
|
+
if (GetFocus() == entry.control) {
|
|
711
|
+
moveFocusToHost();
|
|
712
|
+
}
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
// Out of sight behind a clipping ancestor, which `placement.visible`
|
|
717
|
+
// catches when it is complete and this catches when the control's own
|
|
718
|
+
// strip is what fell outside.
|
|
719
|
+
ShowWindow(entry.control, SW_HIDE);
|
|
720
|
+
entry.regionShape.clear();
|
|
721
|
+
SetWindowRgn(entry.control, nullptr, FALSE);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
|
|
676
725
|
SetWindowPos(entry.control,
|
|
677
726
|
nullptr,
|
|
678
727
|
rect.left,
|
|
@@ -680,6 +729,17 @@ void Win32TextInputManager::syncBounds(RnWin32View *root) {
|
|
|
680
729
|
rect.right - rect.left,
|
|
681
730
|
rect.bottom - rect.top,
|
|
682
731
|
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
|
732
|
+
|
|
733
|
+
if (sameShape(shape, entry.regionShape)) {
|
|
734
|
+
if (region != nullptr) {
|
|
735
|
+
DeleteObject(region);
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
entry.regionShape = shape;
|
|
740
|
+
// A null region removes the one the control had, which is what a field
|
|
741
|
+
// that has come back into the clear needs.
|
|
742
|
+
SetWindowRgn(entry.control, region, TRUE);
|
|
683
743
|
}
|
|
684
744
|
}
|
|
685
745
|
|
|
@@ -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
|
|
@@ -273,6 +277,17 @@ class Win32TextInputManager {
|
|
|
273
277
|
// is set to so that the text sits in the middle of the field.
|
|
274
278
|
LONG lineHeight{0};
|
|
275
279
|
|
|
280
|
+
// The window region last installed, as the rectangles it was built from in
|
|
281
|
+
// the control's own coordinates: the visible box first, then each piece
|
|
282
|
+
// taken out of it. Empty means no region, which is a whole unobstructed
|
|
283
|
+
// field -- the common case, and the one worth not touching.
|
|
284
|
+
//
|
|
285
|
+
// Held because `SetWindowRgn` redraws the control, and `syncBounds` runs
|
|
286
|
+
// after every transaction and every wheel notch: a field in a list being
|
|
287
|
+
// scrolled would be told to redraw on every one of them for a region that
|
|
288
|
+
// has not changed. See `syncBounds` and Win32PeerClip.h.
|
|
289
|
+
std::vector<RECT> regionShape;
|
|
290
|
+
|
|
276
291
|
// The input scope last installed for `keyboardType`, and whether one has
|
|
277
292
|
// been. `SetInputScope` replaces the window's whole scope list, so it is
|
|
278
293
|
// 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
|
|
@@ -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
|