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
package/native/CMakeLists.txt
CHANGED
|
@@ -96,6 +96,10 @@ get_property(BASALT_PACKAGE_WIN32_LINK_LIBRARIES GLOBAL PROPERTY BASALT_PACKAGE_
|
|
|
96
96
|
# with a platform half has nowhere else to be tested: it is compiled into the
|
|
97
97
|
# host binary, so the host's suite is the only binary that has it.
|
|
98
98
|
get_property(BASALT_PACKAGE_TESTS GLOBAL PROPERTY BASALT_PACKAGE_TESTS)
|
|
99
|
+
# And the ones only this host can compile, for the reason the AppKit package
|
|
100
|
+
# gives: BASALT_PACKAGE_TESTS goes to all three suites, and a capability whose
|
|
101
|
+
# platform half links prebuilt Skia has tests the other two cannot build.
|
|
102
|
+
get_property(BASALT_PACKAGE_WIN32_TESTS GLOBAL PROPERTY BASALT_PACKAGE_WIN32_TESTS)
|
|
99
103
|
|
|
100
104
|
if(NOT WIN32)
|
|
101
105
|
# Not an error. A checkout builds every package it finds, and on anything but
|
|
@@ -165,7 +169,12 @@ add_library(basalt_win32_view STATIC
|
|
|
165
169
|
# Where a hidden title bar's pieces are -- the caption buttons and the drag
|
|
166
170
|
# regions. Pure geometry over the view tree, so it is tested here with no
|
|
167
171
|
# window and no React Native.
|
|
168
|
-
win32/Win32TitleBarLayout.cpp
|
|
172
|
+
win32/Win32TitleBarLayout.cpp
|
|
173
|
+
# Which pixels of a child-window peer are still the peer's: a <TextInput>'s
|
|
174
|
+
# EDIT clipped to its scrolled ancestors and cut away where the tree paints
|
|
175
|
+
# over it. Geometry over the view tree for the same reason, and tested the
|
|
176
|
+
# same way.
|
|
177
|
+
win32/Win32PeerClip.cpp)
|
|
169
178
|
target_compile_options(basalt_win32_view PRIVATE ${BASALT_WIN32_WARNINGS})
|
|
170
179
|
target_include_directories(basalt_win32_view PUBLIC win32)
|
|
171
180
|
# The header-only part of core, and only that: FocusRing.h, Backface.h,
|
|
@@ -213,7 +222,10 @@ set(BASALT_WIN32_TEST_SOURCES
|
|
|
213
222
|
# compares; this is where the platform half is checked.
|
|
214
223
|
tests/test_win32_accessibility.cpp
|
|
215
224
|
# A hidden title bar's geometry: the caption buttons and the drag regions.
|
|
216
|
-
tests/test_win32_titlebar.cpp
|
|
225
|
+
tests/test_win32_titlebar.cpp
|
|
226
|
+
# A child-window peer's visible region: clipping and occlusion, which no
|
|
227
|
+
# other host needs because no other host has a widget it cannot order.
|
|
228
|
+
tests/test_win32_peer_clip.cpp)
|
|
217
229
|
|
|
218
230
|
# ---------------------------------------------------------------------------
|
|
219
231
|
# basalt_win32_mounting — the Windows half of the Fabric mounting manager.
|
|
@@ -266,6 +278,16 @@ if(TARGET basalt_core)
|
|
|
266
278
|
# ...and of core/PlatformServices.h, which is also where postToUiThread
|
|
267
279
|
# lives -- the marshalling the mounting manager depends on.
|
|
268
280
|
win32/PlatformServicesWin32.cpp
|
|
281
|
+
# Waiting, over that same marshalling rather than over SetTimer. The
|
|
282
|
+
# header says why there is a second way to wait on a platform that has
|
|
283
|
+
# one: WM_TIMER ranks below WM_PAINT and so does not arrive in an app that
|
|
284
|
+
# paints continuously.
|
|
285
|
+
win32/Win32UiTimer.cpp
|
|
286
|
+
# The message loop, which is this host furniture rather than the mounting
|
|
287
|
+
# manager's -- and is here rather than beside main_win32.cpp so the suite
|
|
288
|
+
# can link it. What it has to get right is subtle enough to be worth a
|
|
289
|
+
# test; see win32/Win32RunLoopObserver.h.
|
|
290
|
+
win32/Win32RunLoopObserver.cpp
|
|
269
291
|
# The window's title bar: DWM's colours and dark mode, and the caption the
|
|
270
292
|
# host draws when an app hides the system's. And BasaltWindow, the module
|
|
271
293
|
# JavaScript reaches it through.
|
|
@@ -302,10 +324,22 @@ if(TARGET basalt_core)
|
|
|
302
324
|
# AppKitSkiaPeer.mm's opposite number. Where that one hands the package a
|
|
303
325
|
# Metal layer, this renders into memory and lets the Direct2D walk draw
|
|
304
326
|
# the result -- this host has no layer tree to hang a canvas in.
|
|
305
|
-
win32/Win32SkiaPeer.cpp
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
327
|
+
win32/Win32SkiaPeer.cpp)
|
|
328
|
+
endif()
|
|
329
|
+
|
|
330
|
+
# The unicode factory the package asks for by name on anything that is not
|
|
331
|
+
# Apple, answered with the backend that is actually linked here.
|
|
332
|
+
#
|
|
333
|
+
# Outside the 2.x block because both generations need it and for the same
|
|
334
|
+
# reason: JsiSkParagraphBuilder takes SkUnicodes::ICU::Make() off Apple, and
|
|
335
|
+
# the Skia this platform builds for Windows carries libgrapheme rather than
|
|
336
|
+
# ICU in either generation. Inside it, a 3.x build -- which makes no
|
|
337
|
+
# skia_core -- linked everything else and then failed on one symbol.
|
|
338
|
+
get_property(BASALT_WIN32_PACKAGE_DEFINITIONS
|
|
339
|
+
GLOBAL PROPERTY BASALT_PACKAGE_DEFINITIONS)
|
|
340
|
+
if(TARGET skia_core OR
|
|
341
|
+
"BASALT_HAS_SKIA_GRAPHITE=1" IN_LIST BASALT_WIN32_PACKAGE_DEFINITIONS)
|
|
342
|
+
target_sources(basalt_win32_mounting PRIVATE win32/Win32SkiaUnicode.cpp)
|
|
309
343
|
endif()
|
|
310
344
|
# shell32 for ShellExecute; comctl32 for SetWindowSubclass, which is how a
|
|
311
345
|
# <TextInput>'s EDIT peer is subclassed without breaking anything else that
|
|
@@ -322,9 +356,6 @@ if(TARGET basalt_core)
|
|
|
322
356
|
# -------------------------------------------------------------------------
|
|
323
357
|
add_executable(basalt_win32
|
|
324
358
|
win32/main_win32.cpp
|
|
325
|
-
# The event beat. Not an observer on Windows: there is no "about to wait"
|
|
326
|
-
# hook, so the message loop itself is here. See the header.
|
|
327
|
-
win32/Win32RunLoopObserver.cpp
|
|
328
359
|
win32/Win32AnimationChoreographer.cpp)
|
|
329
360
|
target_compile_options(basalt_win32 PRIVATE ${BASALT_WIN32_WARNINGS})
|
|
330
361
|
target_link_libraries(basalt_win32 PRIVATE basalt_win32_mounting)
|
|
@@ -339,6 +370,16 @@ if(TARGET basalt_core)
|
|
|
339
370
|
add_executable(mount_harness_win32 win32/mount_harness_win32.cpp)
|
|
340
371
|
target_compile_options(mount_harness_win32 PRIVATE ${BASALT_WIN32_WARNINGS})
|
|
341
372
|
target_link_libraries(mount_harness_win32 PRIVATE basalt_win32_mounting)
|
|
373
|
+
# The same package libraries the host and the suite take, and for a reason
|
|
374
|
+
# that only showed up once a capability contributed something the mounting
|
|
375
|
+
# manager calls: a static library hands over the objects somebody references,
|
|
376
|
+
# so basalt-skia's module sat unreferenced here for as long as nothing but
|
|
377
|
+
# the TurboModule registry asked for it. The 3.x canvas is called straight
|
|
378
|
+
# from updateView, so the harness pulls it in and needs what it links.
|
|
379
|
+
if(BASALT_PACKAGE_WIN32_LINK_LIBRARIES)
|
|
380
|
+
target_link_libraries(mount_harness_win32
|
|
381
|
+
PRIVATE ${BASALT_PACKAGE_WIN32_LINK_LIBRARIES})
|
|
382
|
+
endif()
|
|
342
383
|
|
|
343
384
|
list(APPEND BASALT_WIN32_TEST_SOURCES
|
|
344
385
|
${BASALT_CORE_TESTS}
|
|
@@ -363,6 +404,7 @@ if(TARGET basalt_core)
|
|
|
363
404
|
win32/Win32KeyEvents.cpp
|
|
364
405
|
# A package's own tests; see the AppKit package for why they sit with core's.
|
|
365
406
|
${BASALT_PACKAGE_TESTS}
|
|
407
|
+
${BASALT_PACKAGE_WIN32_TESTS}
|
|
366
408
|
# Deliberately the same nine questions tests/test_mounting.cpp asks of GTK
|
|
367
409
|
# and tests/test_appkit_mounting.mm asks of AppKit, in the same order with
|
|
368
410
|
# the same tags. Most of what they test is whether the sharing still holds.
|
|
@@ -382,6 +424,11 @@ if(TARGET basalt_core)
|
|
|
382
424
|
# innermost-first routing and the commands.
|
|
383
425
|
tests/test_win32_focus.cpp
|
|
384
426
|
tests/test_win32_scrollview.cpp
|
|
427
|
+
# Waiting, and the one condition that made the platform's own way of
|
|
428
|
+
# waiting stop working. See win32/Win32UiTimer.h.
|
|
429
|
+
tests/test_win32_ui_timer.cpp
|
|
430
|
+
# The message loop, and the bound on its drain.
|
|
431
|
+
tests/test_win32_run_loop.cpp
|
|
385
432
|
# <TextInput>. The only tests here that make a window, because a text
|
|
386
433
|
# field's peer is one -- see the file.
|
|
387
434
|
tests/test_win32_textinput.cpp
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
#include <react/renderer/components/view/ViewProps.h>
|
|
26
26
|
|
|
27
27
|
#include <cstdint>
|
|
28
|
+
#include <cstring>
|
|
28
29
|
#include <string>
|
|
29
30
|
#include <vector>
|
|
30
31
|
|
|
@@ -395,6 +396,46 @@ TEST(win32_a_synthesised_hover_walks_the_tree_without_an_emitter) {
|
|
|
395
396
|
// hit-test suite covers it. `auto` and `url` reach a host as no keyword at all.
|
|
396
397
|
// ---------------------------------------------------------------------------
|
|
397
398
|
|
|
399
|
+
// A cursor as pixels, for comparing two of them.
|
|
400
|
+
//
|
|
401
|
+
// DrawIconEx at a size we choose rather than the cursor's own, so that a
|
|
402
|
+
// 32-pixel drawn cursor and whatever size the system hands back are the same
|
|
403
|
+
// shape of answer. False when the cursor cannot be drawn at all, which the
|
|
404
|
+
// caller treats as "nothing to compare" rather than as a failure.
|
|
405
|
+
bool cursorPixels(HCURSOR cursor, std::vector<std::uint8_t> &out) {
|
|
406
|
+
constexpr int kSide = 32;
|
|
407
|
+
HDC screen = GetDC(nullptr);
|
|
408
|
+
HDC dc = CreateCompatibleDC(screen);
|
|
409
|
+
BITMAPINFO info{};
|
|
410
|
+
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
|
411
|
+
info.bmiHeader.biWidth = kSide;
|
|
412
|
+
info.bmiHeader.biHeight = -kSide;
|
|
413
|
+
info.bmiHeader.biPlanes = 1;
|
|
414
|
+
info.bmiHeader.biBitCount = 32;
|
|
415
|
+
info.bmiHeader.biCompression = BI_RGB;
|
|
416
|
+
void *bits = nullptr;
|
|
417
|
+
HBITMAP bitmap = CreateDIBSection(dc, &info, DIB_RGB_COLORS, &bits, nullptr, 0);
|
|
418
|
+
bool ok = false;
|
|
419
|
+
if (bitmap != nullptr && bits != nullptr) {
|
|
420
|
+
HGDIOBJ previous = SelectObject(dc, bitmap);
|
|
421
|
+
// Cleared first: a DIB section is not promised zeroed, and comparing two
|
|
422
|
+
// cursors over two different piles of leftover memory would never match.
|
|
423
|
+
std::memset(bits, 0, static_cast<std::size_t>(kSide) * kSide * 4);
|
|
424
|
+
ok = DrawIconEx(dc, 0, 0, cursor, kSide, kSide, 0, nullptr, DI_NORMAL) != FALSE;
|
|
425
|
+
if (ok) {
|
|
426
|
+
out.assign(static_cast<std::uint8_t *>(bits),
|
|
427
|
+
static_cast<std::uint8_t *>(bits) + static_cast<std::size_t>(kSide) * kSide * 4);
|
|
428
|
+
}
|
|
429
|
+
SelectObject(dc, previous);
|
|
430
|
+
}
|
|
431
|
+
if (bitmap != nullptr) {
|
|
432
|
+
DeleteObject(bitmap);
|
|
433
|
+
}
|
|
434
|
+
DeleteDC(dc);
|
|
435
|
+
ReleaseDC(nullptr, screen);
|
|
436
|
+
return ok;
|
|
437
|
+
}
|
|
438
|
+
|
|
398
439
|
TEST(win32_every_cursor_keyword_resolves_to_a_cursor_of_its_own) {
|
|
399
440
|
// CSS's list, grouped into the families that may legitimately share one
|
|
400
441
|
// cursor. A family is the exception the claim allows; everything else has to
|
|
@@ -490,6 +531,47 @@ TEST(win32_every_cursor_keyword_resolves_to_a_cursor_of_its_own) {
|
|
|
490
531
|
}
|
|
491
532
|
EXPECT_EQ(collided, std::string(""));
|
|
492
533
|
|
|
534
|
+
// And again by what they look like, which is not the same question.
|
|
535
|
+
//
|
|
536
|
+
// The check above compares handles, and two handles are different the
|
|
537
|
+
// moment they come from different calls -- `LoadCursorFromFileW` on a .cur
|
|
538
|
+
// and `LoadCursorW(IDC_HAND)` are two handles to the same drawing. That is
|
|
539
|
+
// exactly how `alias` and `pointer` came to be the same cursor without this
|
|
540
|
+
// test noticing: `alias` was loaded from %SystemRoot%\Cursors\aero_link.cur,
|
|
541
|
+
// which on Windows 11 is "Link Select" -- the hand IDC_HAND already gives
|
|
542
|
+
// `pointer`. Distinct handles, one picture, and the user cannot tell the two
|
|
543
|
+
// keywords apart.
|
|
544
|
+
//
|
|
545
|
+
// So this draws each one and compares the pixels. Thirty-two by thirty-two
|
|
546
|
+
// because that is the size DrawIconEx is being asked for rather than the
|
|
547
|
+
// size the cursor happens to be, which makes a system cursor and a drawn
|
|
548
|
+
// one comparable at all.
|
|
549
|
+
std::string looksTheSame;
|
|
550
|
+
for (const Resolved &left : resolved) {
|
|
551
|
+
for (const Resolved &right : resolved) {
|
|
552
|
+
if (left.family >= right.family) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
if (left.answer.cursor == nullptr || right.answer.cursor == nullptr) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
std::vector<std::uint8_t> leftBits;
|
|
559
|
+
std::vector<std::uint8_t> rightBits;
|
|
560
|
+
if (!cursorPixels(left.answer.cursor, leftBits) ||
|
|
561
|
+
!cursorPixels(right.answer.cursor, rightBits)) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (leftBits != rightBits) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (!looksTheSame.empty()) {
|
|
568
|
+
looksTheSame += ",";
|
|
569
|
+
}
|
|
570
|
+
looksTheSame += left.keyword + "=" + right.keyword;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
EXPECT_EQ(looksTheSame, std::string(""));
|
|
574
|
+
|
|
493
575
|
// And the failure this is really about, named directly rather than left to
|
|
494
576
|
// the loop above: the arrow belongs to `default`. A keyword mapped onto
|
|
495
577
|
// `IDC_ARROW` to stop it answering nothing is the same silence with a handle
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Which pixels of a `<TextInput>`'s EDIT are still the EDIT's.
|
|
2
|
+
//
|
|
3
|
+
// The peer is a child window, so USER32 draws it after everything Direct2D
|
|
4
|
+
// drew and the React tree has no say in it. `peerClip` is what works out the
|
|
5
|
+
// region that puts the say back -- see win32/Win32PeerClip.h -- and it is pure
|
|
6
|
+
// geometry over a view tree, so none of this needs a window, a control or
|
|
7
|
+
// React Native.
|
|
8
|
+
//
|
|
9
|
+
// The two failures it exists to prevent are both things a person sees and a
|
|
10
|
+
// test otherwise does not: a field in a half-scrolled list drawn over the
|
|
11
|
+
// list's neighbour, and a field floating at full brightness over a modal.
|
|
12
|
+
|
|
13
|
+
#include "TestHarness.h"
|
|
14
|
+
|
|
15
|
+
#include "RnWin32View.h"
|
|
16
|
+
#include "Win32PeerClip.h"
|
|
17
|
+
|
|
18
|
+
#include <memory>
|
|
19
|
+
#include <sstream>
|
|
20
|
+
#include <string>
|
|
21
|
+
#include <vector>
|
|
22
|
+
|
|
23
|
+
using basalt::win32::peerClip;
|
|
24
|
+
using basalt::win32::RnWin32View;
|
|
25
|
+
|
|
26
|
+
namespace {
|
|
27
|
+
|
|
28
|
+
class Tree {
|
|
29
|
+
public:
|
|
30
|
+
RnWin32View *box(float x, float y, float w, float h) {
|
|
31
|
+
auto view = std::make_unique<RnWin32View>(nextTag_++);
|
|
32
|
+
view->setFrame(x, y, w, h);
|
|
33
|
+
RnWin32View *raw = view.get();
|
|
34
|
+
views_.push_back(std::move(view));
|
|
35
|
+
return raw;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A box that paints: `alpha` is the background's, which is the whole of what
|
|
39
|
+
// decides whether it covers a field under it.
|
|
40
|
+
RnWin32View *painted(float x, float y, float w, float h, float alpha) {
|
|
41
|
+
RnWin32View *view = box(x, y, w, h);
|
|
42
|
+
view->setBackgroundColor(0.0f, 0.0f, 0.0f, alpha, true);
|
|
43
|
+
return view;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private:
|
|
47
|
+
int32_t nextTag_ = 1;
|
|
48
|
+
std::vector<std::unique_ptr<RnWin32View>> views_;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
std::string describe(const RECT &rect) {
|
|
52
|
+
std::ostringstream out;
|
|
53
|
+
out << rect.left << "," << rect.top << " " << rect.right << "," << rect.bottom;
|
|
54
|
+
return out.str();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
} // namespace
|
|
58
|
+
|
|
59
|
+
TEST(peer_clip_reports_the_field_where_the_tree_puts_it) {
|
|
60
|
+
Tree tree;
|
|
61
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
62
|
+
RnWin32View *pane = tree.box(40, 20, 300, 400);
|
|
63
|
+
RnWin32View *field = tree.box(10, 30, 200, 32);
|
|
64
|
+
root->insertChild(pane, 0);
|
|
65
|
+
pane->insertChild(field, 0);
|
|
66
|
+
|
|
67
|
+
const auto clip = peerClip(field, root);
|
|
68
|
+
EXPECT(clip.visible);
|
|
69
|
+
// Offsets accumulate down the chain, and the root's own frame is not part of
|
|
70
|
+
// them: these are client coordinates.
|
|
71
|
+
EXPECT_EQ(describe(clip.bounds), std::string("50,50 250,82"));
|
|
72
|
+
// Nothing clips and nothing paints over it, so the visible part is all of it.
|
|
73
|
+
EXPECT_EQ(describe(clip.clip), std::string("50,50 250,82"));
|
|
74
|
+
EXPECT_EQ(clip.occluders.size(), static_cast<size_t>(0));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
TEST(peer_clip_narrows_a_field_half_scrolled_out_of_its_list) {
|
|
78
|
+
Tree tree;
|
|
79
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
80
|
+
RnWin32View *list = tree.box(0, 100, 300, 200);
|
|
81
|
+
list->setClipsChildren(true);
|
|
82
|
+
RnWin32View *field = tree.box(0, 180, 300, 40);
|
|
83
|
+
root->insertChild(list, 0);
|
|
84
|
+
list->insertChild(field, 0);
|
|
85
|
+
|
|
86
|
+
// Unscrolled the field is at y=280..320 and the list ends at y=300, so half
|
|
87
|
+
// of it is outside. This is the case the old rule got wrong: it hid a field
|
|
88
|
+
// only once it had left the list *entirely*, so the bottom 20 pixels went on
|
|
89
|
+
// being drawn over whatever came after the list.
|
|
90
|
+
const auto clip = peerClip(field, root);
|
|
91
|
+
EXPECT(clip.visible);
|
|
92
|
+
EXPECT_EQ(describe(clip.bounds), std::string("0,280 300,320"));
|
|
93
|
+
EXPECT_EQ(describe(clip.clip), std::string("0,280 300,300"));
|
|
94
|
+
|
|
95
|
+
// Scrolled until it is gone, and it is gone.
|
|
96
|
+
list->setScrollOffset(0, 400);
|
|
97
|
+
EXPECT(!peerClip(field, root).visible);
|
|
98
|
+
|
|
99
|
+
// And scrolled back so it sits inside, the clip is the whole field again.
|
|
100
|
+
list->setScrollOffset(0, 100);
|
|
101
|
+
const auto inside = peerClip(field, root);
|
|
102
|
+
EXPECT(inside.visible);
|
|
103
|
+
EXPECT_EQ(describe(inside.clip), std::string("0,180 300,220"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
TEST(peer_clip_is_empty_for_a_field_nothing_can_see) {
|
|
107
|
+
Tree tree;
|
|
108
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
109
|
+
RnWin32View *pane = tree.box(0, 0, 300, 400);
|
|
110
|
+
RnWin32View *field = tree.box(0, 0, 200, 32);
|
|
111
|
+
root->insertChild(pane, 0);
|
|
112
|
+
pane->insertChild(field, 0);
|
|
113
|
+
|
|
114
|
+
EXPECT(peerClip(field, root).visible);
|
|
115
|
+
|
|
116
|
+
field->setHidden(true);
|
|
117
|
+
EXPECT(!peerClip(field, root).visible);
|
|
118
|
+
field->setHidden(false);
|
|
119
|
+
|
|
120
|
+
// An ancestor hidden counts the same, which `display: none` on a pane is.
|
|
121
|
+
pane->setHidden(true);
|
|
122
|
+
EXPECT(!peerClip(field, root).visible);
|
|
123
|
+
pane->setHidden(false);
|
|
124
|
+
|
|
125
|
+
// Detached -- between a Remove and its Delete -- is not under the root, so
|
|
126
|
+
// the walk never reaches it.
|
|
127
|
+
pane->removeChild(field);
|
|
128
|
+
EXPECT(!peerClip(field, root).visible);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
TEST(peer_clip_cuts_away_what_the_tree_paints_over_a_field) {
|
|
132
|
+
Tree tree;
|
|
133
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
134
|
+
RnWin32View *pane = tree.box(0, 0, 400, 600);
|
|
135
|
+
RnWin32View *field = tree.box(20, 40, 200, 32);
|
|
136
|
+
// The modal: a dimming backdrop over the whole window, rendered after the
|
|
137
|
+
// panes because that is where an overlay goes in a React tree.
|
|
138
|
+
RnWin32View *backdrop = tree.painted(0, 0, 800, 600, 0.63f);
|
|
139
|
+
root->insertChild(pane, 0);
|
|
140
|
+
pane->insertChild(field, 0);
|
|
141
|
+
root->insertChild(backdrop, 1);
|
|
142
|
+
|
|
143
|
+
const auto clip = peerClip(field, root);
|
|
144
|
+
EXPECT(clip.visible);
|
|
145
|
+
EXPECT_EQ(clip.occluders.size(), static_cast<size_t>(1));
|
|
146
|
+
EXPECT_EQ(describe(clip.occluders[0]), std::string("0,0 800,600"));
|
|
147
|
+
|
|
148
|
+
// A translucent backdrop counts. It has to: the field is a window and the
|
|
149
|
+
// backdrop is paint, so there is no blend the two can take part in, and an
|
|
150
|
+
// undimmed field over a modal is the bug this is here for.
|
|
151
|
+
//
|
|
152
|
+
// Fully transparent does not count -- that is a layout wrapper, which is
|
|
153
|
+
// most of a real tree.
|
|
154
|
+
backdrop->setBackgroundColor(0.0f, 0.0f, 0.0f, 0.0f, true);
|
|
155
|
+
EXPECT_EQ(peerClip(field, root).occluders.size(), static_cast<size_t>(0));
|
|
156
|
+
|
|
157
|
+
// Neither does no background at all, which is not the same thing.
|
|
158
|
+
backdrop->setBackgroundColor(0.0f, 0.0f, 0.0f, 1.0f, false);
|
|
159
|
+
EXPECT_EQ(peerClip(field, root).occluders.size(), static_cast<size_t>(0));
|
|
160
|
+
|
|
161
|
+
// Nor an invisible one, however solid its colour.
|
|
162
|
+
backdrop->setBackgroundColor(0.0f, 0.0f, 0.0f, 1.0f, true);
|
|
163
|
+
backdrop->setOpacity(0.0f);
|
|
164
|
+
EXPECT_EQ(peerClip(field, root).occluders.size(), static_cast<size_t>(0));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
TEST(peer_clip_ignores_everything_painted_before_the_field) {
|
|
168
|
+
Tree tree;
|
|
169
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
170
|
+
// A solid pane behind the field, and the field's own background: both are
|
|
171
|
+
// painted first, and a field is not hidden by what is under it.
|
|
172
|
+
RnWin32View *behind = tree.painted(0, 0, 800, 600, 1.0f);
|
|
173
|
+
RnWin32View *pane = tree.painted(0, 0, 400, 600, 1.0f);
|
|
174
|
+
RnWin32View *field = tree.box(20, 40, 200, 32);
|
|
175
|
+
root->insertChild(behind, 0);
|
|
176
|
+
root->insertChild(pane, 1);
|
|
177
|
+
pane->insertChild(field, 0);
|
|
178
|
+
|
|
179
|
+
const auto clip = peerClip(field, root);
|
|
180
|
+
EXPECT(clip.visible);
|
|
181
|
+
EXPECT_EQ(clip.occluders.size(), static_cast<size_t>(0));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
TEST(peer_clip_follows_z_index_rather_than_the_child_list) {
|
|
185
|
+
Tree tree;
|
|
186
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
187
|
+
RnWin32View *field = tree.box(0, 0, 200, 32);
|
|
188
|
+
RnWin32View *sheet = tree.painted(0, 0, 800, 600, 1.0f);
|
|
189
|
+
// The sheet is inserted first, so by mutation order it is painted first and
|
|
190
|
+
// covers nothing.
|
|
191
|
+
root->insertChild(sheet, 0);
|
|
192
|
+
root->insertChild(field, 1);
|
|
193
|
+
EXPECT_EQ(peerClip(field, root).occluders.size(), static_cast<size_t>(0));
|
|
194
|
+
|
|
195
|
+
// zIndex restacks painting without touching the child list, and the answer
|
|
196
|
+
// has to follow the painting -- which is the same rule hit testing is under.
|
|
197
|
+
sheet->setZIndex(40);
|
|
198
|
+
EXPECT_EQ(peerClip(field, root).occluders.size(), static_cast<size_t>(1));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
TEST(peer_clip_narrows_an_occluder_to_its_own_clipping_ancestor) {
|
|
202
|
+
Tree tree;
|
|
203
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
204
|
+
RnWin32View *field = tree.box(0, 0, 400, 200);
|
|
205
|
+
// A panel after the field whose content is twice its height and clipped to
|
|
206
|
+
// it. What covers the field is the panel's visible part, not the child's
|
|
207
|
+
// full box -- the half outside the panel is not drawn anywhere.
|
|
208
|
+
RnWin32View *panel = tree.box(0, 0, 400, 100);
|
|
209
|
+
panel->setClipsChildren(true);
|
|
210
|
+
RnWin32View *content = tree.painted(0, 0, 400, 200, 1.0f);
|
|
211
|
+
root->insertChild(field, 0);
|
|
212
|
+
root->insertChild(panel, 1);
|
|
213
|
+
panel->insertChild(content, 0);
|
|
214
|
+
|
|
215
|
+
const auto clip = peerClip(field, root);
|
|
216
|
+
EXPECT_EQ(clip.occluders.size(), static_cast<size_t>(1));
|
|
217
|
+
EXPECT_EQ(describe(clip.occluders[0]), std::string("0,0 400,100"));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
TEST(peer_clip_leaves_a_transformed_view_alone) {
|
|
221
|
+
Tree tree;
|
|
222
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
223
|
+
RnWin32View *field = tree.box(0, 0, 200, 32);
|
|
224
|
+
RnWin32View *card = tree.painted(0, 0, 800, 600, 1.0f);
|
|
225
|
+
root->insertChild(field, 0);
|
|
226
|
+
root->insertChild(card, 1);
|
|
227
|
+
EXPECT_EQ(peerClip(field, root).occluders.size(), static_cast<size_t>(1));
|
|
228
|
+
|
|
229
|
+
// Rotated, its box is no longer a rectangle in these coordinates, and a
|
|
230
|
+
// region is rectangles. Guessing at a bounding box would hide more of the
|
|
231
|
+
// field than the card covers, so nothing is cut away at all -- the same
|
|
232
|
+
// decision the placement already makes about a transformed field.
|
|
233
|
+
const float rotated[16] = {0.0f,
|
|
234
|
+
1.0f,
|
|
235
|
+
0.0f,
|
|
236
|
+
0.0f,
|
|
237
|
+
-1.0f,
|
|
238
|
+
0.0f,
|
|
239
|
+
0.0f,
|
|
240
|
+
0.0f,
|
|
241
|
+
0.0f,
|
|
242
|
+
0.0f,
|
|
243
|
+
1.0f,
|
|
244
|
+
0.0f,
|
|
245
|
+
0.0f,
|
|
246
|
+
0.0f,
|
|
247
|
+
0.0f,
|
|
248
|
+
1.0f};
|
|
249
|
+
card->setTransform(rotated);
|
|
250
|
+
EXPECT_EQ(peerClip(field, root).occluders.size(), static_cast<size_t>(0));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
TEST(peer_clip_does_not_treat_the_fields_own_children_as_over_it) {
|
|
254
|
+
Tree tree;
|
|
255
|
+
RnWin32View *root = tree.box(0, 0, 800, 600);
|
|
256
|
+
RnWin32View *field = tree.box(0, 0, 200, 32);
|
|
257
|
+
RnWin32View *inside = tree.painted(0, 0, 200, 32, 1.0f);
|
|
258
|
+
root->insertChild(field, 0);
|
|
259
|
+
field->insertChild(inside, 0);
|
|
260
|
+
|
|
261
|
+
// Inside the field is not over it, and a field that cut itself away for its
|
|
262
|
+
// own content would never be visible at all.
|
|
263
|
+
const auto clip = peerClip(field, root);
|
|
264
|
+
EXPECT(clip.visible);
|
|
265
|
+
EXPECT_EQ(clip.occluders.size(), static_cast<size_t>(0));
|
|
266
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// The message loop, and the one thing it has to get right that is not
|
|
2
|
+
// obvious. See win32/Win32RunLoopObserver.h.
|
|
3
|
+
|
|
4
|
+
#include "TestHarness.h"
|
|
5
|
+
|
|
6
|
+
#include "Win32RunLoopObserver.h"
|
|
7
|
+
|
|
8
|
+
#include <windows.h>
|
|
9
|
+
|
|
10
|
+
#include <chrono>
|
|
11
|
+
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
constexpr UINT kFlood = WM_APP + 11;
|
|
15
|
+
|
|
16
|
+
// A message-only window whose only message reposts itself, so the queue is
|
|
17
|
+
// never observed empty. `flooding` ends it, since a message still in the
|
|
18
|
+
// queue when the loop stops would be pumped by whatever runs next.
|
|
19
|
+
bool gFlooding = true;
|
|
20
|
+
long gFlooded = 0;
|
|
21
|
+
|
|
22
|
+
LRESULT CALLBACK floodProc(HWND hwnd, UINT message, WPARAM w, LPARAM l) {
|
|
23
|
+
if (message == kFlood) {
|
|
24
|
+
gFlooded++;
|
|
25
|
+
// A ceiling, so that a loop which never beats fails rather than hangs:
|
|
26
|
+
// the beat is what stops the flood and posts WM_QUIT, and a regression
|
|
27
|
+
// here is precisely a beat that never happens.
|
|
28
|
+
if (gFlooded > 50000) {
|
|
29
|
+
gFlooding = false;
|
|
30
|
+
}
|
|
31
|
+
if (gFlooding) {
|
|
32
|
+
PostMessageW(hwnd, kFlood, 0, 0);
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
return DefWindowProcW(hwnd, message, w, l);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
HWND makeFloodWindow() {
|
|
40
|
+
WNDCLASSEXW windowClass{};
|
|
41
|
+
windowClass.cbSize = sizeof(windowClass);
|
|
42
|
+
windowClass.lpfnWndProc = floodProc;
|
|
43
|
+
windowClass.hInstance = GetModuleHandleW(nullptr);
|
|
44
|
+
windowClass.lpszClassName = L"BasaltRunLoopFloodTest";
|
|
45
|
+
RegisterClassExW(&windowClass);
|
|
46
|
+
return CreateWindowExW(0,
|
|
47
|
+
L"BasaltRunLoopFloodTest",
|
|
48
|
+
L"",
|
|
49
|
+
0,
|
|
50
|
+
0,
|
|
51
|
+
0,
|
|
52
|
+
0,
|
|
53
|
+
0,
|
|
54
|
+
HWND_MESSAGE,
|
|
55
|
+
nullptr,
|
|
56
|
+
GetModuleHandleW(nullptr),
|
|
57
|
+
nullptr);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
void dropFloodWindow(HWND window) {
|
|
61
|
+
gFlooding = false;
|
|
62
|
+
DestroyWindow(window);
|
|
63
|
+
UnregisterClassW(L"BasaltRunLoopFloodTest", GetModuleHandleW(nullptr));
|
|
64
|
+
// Whatever is left of the flood, so the next test's pump does not inherit
|
|
65
|
+
// it.
|
|
66
|
+
MSG message;
|
|
67
|
+
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
} // namespace
|
|
72
|
+
|
|
73
|
+
TEST(run_loop_beats_once_when_the_queue_empties) {
|
|
74
|
+
int beats = 0;
|
|
75
|
+
PostMessageW(nullptr, WM_NULL, 0, 0);
|
|
76
|
+
const int code = basalt::runMessageLoop([&beats] {
|
|
77
|
+
beats++;
|
|
78
|
+
PostQuitMessage(7);
|
|
79
|
+
});
|
|
80
|
+
EXPECT_EQ(code, 7);
|
|
81
|
+
EXPECT_EQ(beats, 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
TEST(run_loop_beats_on_a_queue_that_never_empties) {
|
|
85
|
+
// The regression this file exists for.
|
|
86
|
+
//
|
|
87
|
+
// The loop drains before it beats, which is what "BeforeWaiting" means and
|
|
88
|
+
// what React Native's event queue is flushed by. An app that paints
|
|
89
|
+
// continuously -- kino, whose preview is a Skia `<Canvas>`: 120 WM_PAINT
|
|
90
|
+
// and 250 posted work items a second while idle -- never lets the drain
|
|
91
|
+
// finish, so the beat never ran and events sat in the queue. What that
|
|
92
|
+
// looked like from outside was a button that usually worked: one scripted
|
|
93
|
+
// tap in three did nothing, and a real click rather more often than that.
|
|
94
|
+
//
|
|
95
|
+
// Without the bound in the loop this test does not fail slowly. It never
|
|
96
|
+
// beats at all, so it never posts WM_QUIT, and the deadline below is the
|
|
97
|
+
// only thing that ends it.
|
|
98
|
+
gFlooding = true;
|
|
99
|
+
gFlooded = 0;
|
|
100
|
+
HWND window = makeFloodWindow();
|
|
101
|
+
EXPECT(window != nullptr);
|
|
102
|
+
PostMessageW(window, kFlood, 0, 0);
|
|
103
|
+
|
|
104
|
+
int beats = 0;
|
|
105
|
+
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
|
106
|
+
basalt::runMessageLoop([&] {
|
|
107
|
+
beats++;
|
|
108
|
+
// Several, so this is about a loop that keeps beating rather than one
|
|
109
|
+
// that beat once on the way in.
|
|
110
|
+
if (beats >= 4 || std::chrono::steady_clock::now() > deadline) {
|
|
111
|
+
gFlooding = false;
|
|
112
|
+
PostQuitMessage(0);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
EXPECT(beats >= 4);
|
|
117
|
+
// ...and the flood really was running, so the beats above happened in spite
|
|
118
|
+
// of it rather than after it ran out.
|
|
119
|
+
EXPECT(gFlooded > 100);
|
|
120
|
+
|
|
121
|
+
dropFloodWindow(window);
|
|
122
|
+
}
|