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.
Files changed (36) hide show
  1. package/native/CMakeLists.txt +56 -9
  2. package/native/tests/test_win32_accessibility.cpp +77 -0
  3. package/native/tests/test_win32_focus.cpp +26 -0
  4. package/native/tests/test_win32_input.cpp +82 -0
  5. package/native/tests/test_win32_peer_clip.cpp +266 -0
  6. package/native/tests/test_win32_run_loop.cpp +122 -0
  7. package/native/tests/test_win32_textinput.cpp +199 -0
  8. package/native/tests/test_win32_ui_timer.cpp +195 -0
  9. package/native/win32/PlatformServicesWin32.cpp +16 -0
  10. package/native/win32/RnWin32Accessible.cpp +27 -0
  11. package/native/win32/RnWin32Accessible.h +5 -0
  12. package/native/win32/RnWin32View.h +18 -0
  13. package/native/win32/Win32AnimationChoreographer.cpp +15 -32
  14. package/native/win32/Win32AnimationChoreographer.h +9 -6
  15. package/native/win32/Win32AppWindow.h +36 -0
  16. package/native/win32/Win32Cursors.cpp +13 -17
  17. package/native/win32/Win32DropTarget.cpp +4 -1
  18. package/native/win32/Win32MenuBar.cpp +3 -1
  19. package/native/win32/Win32MountingManager.cpp +82 -7
  20. package/native/win32/Win32MountingManager.h +5 -0
  21. package/native/win32/Win32PeerClip.cpp +153 -0
  22. package/native/win32/Win32PeerClip.h +92 -0
  23. package/native/win32/Win32RunLoopObserver.cpp +39 -9
  24. package/native/win32/Win32RunLoopObserver.h +41 -2
  25. package/native/win32/Win32ScrollView.cpp +13 -32
  26. package/native/win32/Win32ScrollView.h +5 -6
  27. package/native/win32/Win32SkiaPeer.h +26 -4
  28. package/native/win32/Win32TextInput.cpp +139 -48
  29. package/native/win32/Win32TextInput.h +28 -8
  30. package/native/win32/Win32UiThread.h +11 -0
  31. package/native/win32/Win32UiTimer.cpp +139 -0
  32. package/native/win32/Win32UiTimer.h +65 -0
  33. package/native/win32/Win32WindowControl.cpp +38 -15
  34. package/native/win32/Win32WindowModule.cpp +8 -13
  35. package/native/win32/main_win32.cpp +123 -96
  36. package/package.json +2 -2
@@ -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
- # The unicode factory the package asks for by name on anything that is
307
- # not Apple, answered with the backend that is actually linked here.
308
- win32/Win32SkiaUnicode.cpp)
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
@@ -330,6 +330,83 @@ TEST(accessibility_a_plain_view_claims_no_dialog) {
330
330
  EXPECT_EQ(propertyType(provider, UIA_IsDialogPropertyId), VT_EMPTY);
331
331
  }
332
332
 
333
+ // ---------------------------------------------------------------------------
334
+ // `accessibilityLanguage`, which is the one accessibility prop where this
335
+ // platform does not take the value the app wrote.
336
+ //
337
+ // Every other host publishes the BCP-47 tag verbatim -- AppKit as the
338
+ // `AXLanguage` attribute, and GTK not at all, having no accessible language
339
+ // property. UIA's culture property is an LCID, a number, so the tag has to be
340
+ // converted; `LocaleNameToLCID` is the conversion and these pin that the
341
+ // provider does it rather than answering with the string or with zero.
342
+ //
343
+ // The expected numbers are Windows' own and not invented here: 1036 is fr-FR
344
+ // and 12 is the neutral French. Asserted as `LocaleNameToLCID` reports them,
345
+ // because a hard-coded constant that disagreed with the running system would
346
+ // be testing the wrong thing.
347
+
348
+ TEST(accessibility_a_language_becomes_a_culture_id) {
349
+ RnAccessibleInfo info = withRole("text");
350
+ info.label = "Bonjour";
351
+ info.language = "fr-FR";
352
+
353
+ auto provider = providerFor(info);
354
+ EXPECT(provider != nullptr);
355
+ EXPECT_EQ(propertyType(provider, UIA_CulturePropertyId), VT_I4);
356
+ EXPECT_EQ(longProperty(provider, UIA_CulturePropertyId),
357
+ static_cast<long>(LocaleNameToLCID(L"fr-FR", LOCALE_ALLOW_NEUTRAL_NAMES)));
358
+ // Not the system's, which is what a provider that ignored the prop and
359
+ // answered zero would be claiming.
360
+ EXPECT(longProperty(provider, UIA_CulturePropertyId) != 0);
361
+ }
362
+
363
+ // A tag with no region is what an app writes when it means the language rather
364
+ // than a country, and it is the case `LOCALE_ALLOW_NEUTRAL_NAMES` exists for:
365
+ // without the flag this answers nothing for a large share of what apps write.
366
+ TEST(accessibility_a_language_without_a_region_still_converts) {
367
+ RnAccessibleInfo info = withRole("text");
368
+ info.language = "fr";
369
+
370
+ auto provider = providerFor(info);
371
+ EXPECT(provider != nullptr);
372
+ EXPECT_EQ(propertyType(provider, UIA_CulturePropertyId), VT_I4);
373
+ EXPECT_EQ(longProperty(provider, UIA_CulturePropertyId),
374
+ static_cast<long>(LocaleNameToLCID(L"fr", LOCALE_ALLOW_NEUTRAL_NAMES)));
375
+ }
376
+
377
+ // Unset rather than zero, for the reason the dialog property is unset rather
378
+ // than false: zero is a valid LCID meaning the system default, so answering it
379
+ // would say the app asked for the system language when it asked for nothing.
380
+ TEST(accessibility_no_language_leaves_the_culture_unset) {
381
+ auto provider = providerFor(withRole("text"));
382
+ EXPECT(provider != nullptr);
383
+ EXPECT_EQ(propertyType(provider, UIA_CulturePropertyId), VT_EMPTY);
384
+ }
385
+
386
+ // And the same for a tag this Windows cannot resolve, which is the branch that
387
+ // would otherwise publish zero and call it the app's choice.
388
+ //
389
+ // Over-long rather than merely wrong, and that is deliberate: Windows accepts
390
+ // far more than the tags it has data for -- a well-formed name it does not
391
+ // know can come back as `LOCALE_CUSTOM_UNSPECIFIED` rather than as a failure,
392
+ // and which names those are differs by Windows version. A name longer than
393
+ // `LOCALE_NAME_MAX_LENGTH` is the one input documented to fail everywhere, so
394
+ // it is what pins the branch without the assertion depending on the runner's
395
+ // locale data.
396
+ TEST(accessibility_an_unresolvable_language_leaves_the_culture_unset) {
397
+ RnAccessibleInfo info = withRole("text");
398
+ info.language = std::string(LOCALE_NAME_MAX_LENGTH + 10, 'x');
399
+ // The premise, asserted rather than assumed: if some future Windows resolved
400
+ // this, the test below would be checking nothing.
401
+ EXPECT_EQ(LocaleNameToLCID(basalt::win32::widen(info.language).c_str(),
402
+ LOCALE_ALLOW_NEUTRAL_NAMES),
403
+ static_cast<LCID>(0));
404
+
405
+ auto provider = providerFor(info);
406
+ EXPECT(provider != nullptr);
407
+ EXPECT_EQ(propertyType(provider, UIA_CulturePropertyId), VT_EMPTY);
408
+ }
409
+
333
410
  // ---------------------------------------------------------------------------
334
411
  // `accessibilityLabelledBy`, which is the one accessibility prop that is a
335
412
  // relation between two views rather than a value on one.
@@ -90,6 +90,32 @@ TEST(win32_focus_accessible_views_are_the_ones_tab_stops_on) {
90
90
  manager.destroySurfaceRoot(kSurfaceId);
91
91
  }
92
92
 
93
+ // `accessibilityRespondsToUserInteraction`, which React Native documents as
94
+ // blocking the keyboard while leaving the view accessible. On a desktop that
95
+ // is the Tab order; core/KeyboardFocusable.h decides it for all three hosts
96
+ // and this is the wiring.
97
+ TEST(win32_focus_a_view_that_refuses_interaction_is_not_a_tab_stop) {
98
+ Win32MountingManager manager;
99
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
100
+ root->setFrame(0, 0, 400, 400);
101
+
102
+ ShadowView quiet = makeAccessibleView(10, 0, true);
103
+ auto props = std::make_shared<ViewProps>(*std::static_pointer_cast<const ViewProps>(quiet.props));
104
+ props->accessibilityRespondsToUserInteraction = false;
105
+ quiet.props = props;
106
+
107
+ mountAll(manager, {quiet, makeAccessibleView(11, 50, true)});
108
+
109
+ EXPECT(!manager.viewForTag(10)->focusable());
110
+ // The view beside it said nothing, and the prop defaults to true: one view
111
+ // refusing the keyboard must not take its neighbours with it.
112
+ EXPECT(manager.viewForTag(11)->focusable());
113
+ // And the dump says so, which is what the cross-host comparison reads.
114
+ EXPECT(root->describeTree().find("focusable") != std::string::npos);
115
+
116
+ manager.destroySurfaceRoot(kSurfaceId);
117
+ }
118
+
93
119
  TEST(win32_focus_tab_visits_focusable_views_in_tree_order_and_wraps) {
94
120
  Win32MountingManager manager;
95
121
  RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
@@ -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
+ }