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.
@@ -33,6 +33,8 @@
33
33
 
34
34
  #include <cstdint>
35
35
  #include <memory>
36
+ #include <optional>
37
+ #include <sstream>
36
38
  #include <string>
37
39
  #include <utility>
38
40
  #include <vector>
@@ -326,6 +328,34 @@ RECT rectOf(HWND control) {
326
328
  return rect;
327
329
  }
328
330
 
331
+ // The window region installed on a control, as the box around it, in the
332
+ // control's own coordinates. Absent when the control has no region at all,
333
+ // which is what a whole unobstructed field gets.
334
+ //
335
+ // An empty box -- `0,0 0,0` -- is a region with nothing in it, which is a
336
+ // field that is entirely behind something.
337
+ std::optional<RECT> regionBoxOf(HWND control) {
338
+ HRGN region = CreateRectRgn(0, 0, 0, 0);
339
+ const int kind = GetWindowRgn(control, region);
340
+ std::optional<RECT> box;
341
+ // GDI's `ERROR` is zero, and the name is not reachable here: this build
342
+ // defines GLOG_NO_ABBREVIATED_SEVERITIES, which is what keeps glog from
343
+ // fighting wingdi.h over it, and wingdi.h's own definition loses to it.
344
+ if (kind != 0) {
345
+ RECT rect{0, 0, 0, 0};
346
+ GetRgnBox(region, &rect);
347
+ box = rect;
348
+ }
349
+ DeleteObject(region);
350
+ return box;
351
+ }
352
+
353
+ std::string describeRect(const RECT &rect) {
354
+ std::ostringstream out;
355
+ out << rect.left << "," << rect.top << " " << rect.right << "," << rect.bottom;
356
+ return out.str();
357
+ }
358
+
329
359
  } // namespace
330
360
 
331
361
  // A mounted field gets a real control, and the host's window is its parent.
@@ -576,6 +606,84 @@ TEST(win32_a_peer_scrolled_out_of_view_is_hidden) {
576
606
  manager->destroySurfaceRoot(kSurfaceId);
577
607
  }
578
608
 
609
+ // Half out of its list is half drawn, which hiding it entirely was never the
610
+ // same as. A window region is what says so to USER32; see Win32PeerClip.h.
611
+ //
612
+ // The field is multiline so that the control fills its box exactly -- a
613
+ // single-line peer is a strip the height of one line, centred, and these
614
+ // numbers would then be the font's.
615
+ TEST(win32_a_peer_half_out_of_its_list_is_clipped_to_it) {
616
+ auto manager = makeManager();
617
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
618
+ root->setFrame(0, 0, 800, 600);
619
+ mount(*manager, kSurfaceId, makeView(10, 0, 0, 400, 100));
620
+ manager->viewForTag(10)->setClipsChildren(true);
621
+ mount(*manager, 10, makeField(11, 0, 60, 200, 80, {.multiline = true}));
622
+ manager->syncTextInputBounds(root);
623
+
624
+ HWND control = peerOf(manager->viewForTag(11));
625
+ EXPECT_EQ(describeRect(rectOf(control)), std::string("0,60 200,140"));
626
+ // The list ends at y=100, so the bottom 40 of the field is not the field's.
627
+ const auto clipped = regionBoxOf(control);
628
+ EXPECT(clipped.has_value());
629
+ EXPECT_EQ(describeRect(clipped.value_or(RECT{})), std::string("0,0 200,40"));
630
+
631
+ // Scrolled until it is wholly inside, the region goes away rather than
632
+ // becoming the whole control: SetWindowRgn redraws, and a field in a list
633
+ // being scrolled would otherwise be told to on every notch.
634
+ manager->viewForTag(10)->setScrollOffset(0, 60);
635
+ manager->syncTextInputBounds(root);
636
+ EXPECT(!regionBoxOf(control).has_value());
637
+
638
+ manager->destroySurfaceRoot(kSurfaceId);
639
+ }
640
+
641
+ // A field cannot be covered by anything the tree paints after it, because the
642
+ // peer is a child window and USER32 draws those last. Open a modal over a pane
643
+ // with a field in it and the field floats on top of the modal, still editable.
644
+ // Taking the modal's box out of the region is what stops that.
645
+ TEST(win32_a_peer_under_something_painted_later_is_cut_away) {
646
+ auto manager = makeManager();
647
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
648
+ root->setFrame(0, 0, 800, 600);
649
+ // The backdrop is mounted first and the field second, and both go in at
650
+ // index 0 -- so the children end up [field, backdrop] and the backdrop is
651
+ // painted after, which is where an overlay sits in a React tree.
652
+ mount(*manager, kSurfaceId, makeView(10, 0, 0, 800, 600));
653
+ mount(*manager, kSurfaceId, makeField(11, 0, 0, 200, 80, {.multiline = true}));
654
+ manager->syncTextInputBounds(root);
655
+
656
+ HWND control = peerOf(manager->viewForTag(11));
657
+ // A layout wrapper with no background of its own covers nothing, which is
658
+ // most of a real tree and the reason this rule is quiet in one.
659
+ EXPECT(!regionBoxOf(control).has_value());
660
+
661
+ // kino's modal scrim: #000000A0 over the whole window. Translucent counts --
662
+ // a child window cannot be dimmed by paint, so the choice is between showing
663
+ // it undimmed and hiding it, and hiding it is the better wrong answer.
664
+ manager->viewForTag(10)->setBackgroundColor(0.0f, 0.0f, 0.0f, 0.63f, true);
665
+ manager->syncTextInputBounds(root);
666
+ // Wholly behind it is hidden rather than given a region with nothing in it:
667
+ // the two look the same and only one of them stops Windows thinking there is
668
+ // a window there.
669
+ EXPECT(!IsWindowVisible(control));
670
+ EXPECT(!regionBoxOf(control).has_value());
671
+
672
+ // Half over it leaves the other half.
673
+ manager->viewForTag(10)->setFrame(0, 40, 800, 600);
674
+ manager->syncTextInputBounds(root);
675
+ const auto half = regionBoxOf(control);
676
+ EXPECT(half.has_value());
677
+ EXPECT_EQ(describeRect(half.value_or(RECT{})), std::string("0,0 200,40"));
678
+
679
+ // And once the modal closes the region goes with it.
680
+ manager->viewForTag(10)->setBackgroundColor(0.0f, 0.0f, 0.0f, 0.0f, false);
681
+ manager->syncTextInputBounds(root);
682
+ EXPECT(!regionBoxOf(control).has_value());
683
+
684
+ manager->destroySurfaceRoot(kSurfaceId);
685
+ }
686
+
579
687
  // focus, blur and setTextAndSelection, which arrive through dispatchCommand.
580
688
  TEST(win32_the_commands_reach_the_control) {
581
689
  auto manager = makeManager();
@@ -0,0 +1,195 @@
1
+ // Timers that arrive, including under the one condition that broke the ones
2
+ // before them. See win32/Win32UiTimer.h.
3
+
4
+ #include "TestHarness.h"
5
+
6
+ #include "Win32UiThread.h"
7
+ #include "Win32UiTimer.h"
8
+
9
+ #include <windows.h>
10
+
11
+ #include <chrono>
12
+ #include <functional>
13
+ #include <thread>
14
+
15
+ namespace {
16
+
17
+ // A real message loop for the duration of one test. Without one there is
18
+ // nothing to post to, `scheduleUiTimer` declines, and every assertion below
19
+ // would be about the declining rather than about the timer.
20
+ struct ScopedUiThread {
21
+ ScopedUiThread() { basalt::installUiThread(); }
22
+ ~ScopedUiThread() { basalt::uninstallUiThread(); }
23
+ ScopedUiThread(const ScopedUiThread &) = delete;
24
+ ScopedUiThread &operator=(const ScopedUiThread &) = delete;
25
+ };
26
+
27
+ // Pumps until `ready`, or until the deadline. Returns whether it came true.
28
+ //
29
+ // The inner drain is bounded rather than "until the queue is empty", which
30
+ // the real loop can afford and this cannot: one test here feeds the queue a
31
+ // message that reposts itself, and an unbounded drain never comes back out.
32
+ bool pumpUntil(const std::function<bool()> &ready) {
33
+ const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
34
+ while (std::chrono::steady_clock::now() < deadline) {
35
+ MSG message;
36
+ for (int drained = 0; drained < 64; drained++) {
37
+ if (!PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
38
+ break;
39
+ }
40
+ TranslateMessage(&message);
41
+ DispatchMessageW(&message);
42
+ }
43
+ if (ready()) {
44
+ return true;
45
+ }
46
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
47
+ }
48
+ return false;
49
+ }
50
+
51
+ } // namespace
52
+
53
+ TEST(ui_timer_fires_once_after_its_delay) {
54
+ ScopedUiThread loop;
55
+ int fired = 0;
56
+ const basalt::UiTimerId timer = basalt::scheduleUiTimer(10, false, [&fired] { fired++; });
57
+ EXPECT(timer != 0);
58
+ EXPECT(pumpUntil([&fired] { return fired > 0; }));
59
+ // And exactly once: a one-shot that repeated would be found by nothing else
60
+ // here, because every other assertion is satisfied by the first call.
61
+ const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(120);
62
+ pumpUntil([&until] { return std::chrono::steady_clock::now() > until; });
63
+ EXPECT_EQ(fired, 1);
64
+ basalt::cancelUiTimer(timer);
65
+ }
66
+
67
+ TEST(ui_timer_repeats_until_it_is_cancelled) {
68
+ ScopedUiThread loop;
69
+ int fired = 0;
70
+ const basalt::UiTimerId timer = basalt::scheduleUiTimer(5, true, [&fired] { fired++; });
71
+ EXPECT(timer != 0);
72
+ EXPECT(pumpUntil([&fired] { return fired >= 3; }));
73
+ basalt::cancelUiTimer(timer);
74
+ const int afterCancel = fired;
75
+ const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(80);
76
+ pumpUntil([&until] { return std::chrono::steady_clock::now() > until; });
77
+ EXPECT_EQ(fired, afterCancel);
78
+ }
79
+
80
+ TEST(ui_timer_can_be_cancelled_from_inside_its_own_callback) {
81
+ // How a repeating animation ends: the last frame stops the thing that
82
+ // delivered it. `cancelUiTimer` waits out a pool-thread delivery, so doing
83
+ // this on the wrong thread would deadlock rather than misbehave.
84
+ ScopedUiThread loop;
85
+ int fired = 0;
86
+ basalt::UiTimerId timer = 0;
87
+ timer = basalt::scheduleUiTimer(5, true, [&fired, &timer] {
88
+ fired++;
89
+ if (fired == 2) {
90
+ basalt::cancelUiTimer(timer);
91
+ }
92
+ });
93
+ EXPECT(pumpUntil([&fired] { return fired >= 2; }));
94
+ const auto until = std::chrono::steady_clock::now() + std::chrono::milliseconds(80);
95
+ pumpUntil([&until] { return std::chrono::steady_clock::now() > until; });
96
+ EXPECT_EQ(fired, 2);
97
+ }
98
+
99
+ TEST(ui_timer_arrives_on_a_queue_that_never_empties) {
100
+ // The whole reason this file exists.
101
+ //
102
+ // `WM_TIMER` is not posted. It is *synthesised* by `PeekMessage`, and only
103
+ // once the queue holds nothing else -- below posted messages, below input,
104
+ // below `WM_PAINT`. A loop that always has something to do therefore never
105
+ // gives a `WM_TIMER` the gap it needs, and every `SetTimer` in the host
106
+ // simply stopped. That is the ordinary condition of a real app: measured on
107
+ // kino, whose preview is a Skia `<Canvas>`, 120 `WM_PAINT` and 250 posted
108
+ // work items a second while idle, against six `WM_TIMER` in twenty-five
109
+ // seconds.
110
+ //
111
+ // Reproduced here with a message that reposts itself, which is the same
112
+ // queue condition as the painting without needing a window on screen.
113
+ ScopedUiThread loop;
114
+
115
+ constexpr UINT kFlood = WM_APP + 7;
116
+ static long flooded = 0;
117
+ static long ticked = 0;
118
+ // Cleared before the window goes, so the self-reposting message stops
119
+ // rather than outliving this test in the queue the next one pumps.
120
+ static bool flooding = true;
121
+ flooded = 0;
122
+ ticked = 0;
123
+ flooding = true;
124
+
125
+ WNDCLASSEXW windowClass{};
126
+ windowClass.cbSize = sizeof(windowClass);
127
+ windowClass.lpfnWndProc = [](HWND hwnd, UINT message, WPARAM w, LPARAM l) -> LRESULT {
128
+ if (message == kFlood) {
129
+ flooded++;
130
+ // Posted again before this one is finished with, so the queue is never
131
+ // observed empty. Which is the condition under test.
132
+ if (flooding) {
133
+ PostMessageW(hwnd, kFlood, 0, 0);
134
+ }
135
+ return 0;
136
+ }
137
+ if (message == WM_TIMER) {
138
+ ticked++;
139
+ return 0;
140
+ }
141
+ return DefWindowProcW(hwnd, message, w, l);
142
+ };
143
+ windowClass.hInstance = GetModuleHandleW(nullptr);
144
+ windowClass.lpszClassName = L"BasaltUiTimerBusyTest";
145
+ RegisterClassExW(&windowClass);
146
+ HWND window = CreateWindowExW(0,
147
+ L"BasaltUiTimerBusyTest",
148
+ L"",
149
+ 0,
150
+ 0,
151
+ 0,
152
+ 0,
153
+ 0,
154
+ HWND_MESSAGE,
155
+ nullptr,
156
+ GetModuleHandleW(nullptr),
157
+ nullptr);
158
+ EXPECT(window != nullptr);
159
+ PostMessageW(window, kFlood, 0, 0);
160
+
161
+ // The platform's own timer, for the comparison. Ten milliseconds, so a
162
+ // working one would fire many times over what follows.
163
+ SetTimer(window, 1, 10, nullptr);
164
+
165
+ int fired = 0;
166
+ const basalt::UiTimerId timer = basalt::scheduleUiTimer(10, false, [&fired] { fired++; });
167
+ EXPECT(pumpUntil([&fired] { return fired > 0; }));
168
+
169
+ // The flood really did run, so the pass above means the timer arrived
170
+ // through a busy queue rather than through a quiet one.
171
+ EXPECT(flooded > 100);
172
+ // ...and the comparison. If this ever starts firing, Windows has changed
173
+ // its message priorities and this whole file is worth re-reading rather
174
+ // than the assertion being worth relaxing.
175
+ EXPECT_EQ(static_cast<int>(ticked), 0);
176
+
177
+ flooding = false;
178
+ KillTimer(window, 1);
179
+ basalt::cancelUiTimer(timer);
180
+ DestroyWindow(window);
181
+ UnregisterClassW(L"BasaltUiTimerBusyTest", GetModuleHandleW(nullptr));
182
+ }
183
+
184
+ TEST(ui_timer_declines_when_there_is_no_loop_to_post_to) {
185
+ // No ScopedUiThread. Delivering here would mean running the callback on a
186
+ // thread pool, which is how the rest of this suite -- written against
187
+ // inline posting on one thread -- would start failing at random.
188
+ int fired = 0;
189
+ EXPECT_EQ(static_cast<int>(basalt::scheduleUiTimer(1, false, [&fired] { fired++; })), 0);
190
+ std::this_thread::sleep_for(std::chrono::milliseconds(30));
191
+ EXPECT_EQ(fired, 0);
192
+ // And cancelling nothing is allowed, which is what lets a caller keep one
193
+ // id in a field and cancel it without asking whether it ever started.
194
+ basalt::cancelUiTimer(0);
195
+ }
@@ -116,6 +116,22 @@ bool hasUiThread() {
116
116
  return uiThread().window != nullptr;
117
117
  }
118
118
 
119
+ void uninstallUiThread() {
120
+ HWND window = uiThread().window;
121
+ if (window == nullptr) {
122
+ return;
123
+ }
124
+ // Cleared before the window is destroyed, so that anything posted from
125
+ // another thread between these two lines runs inline rather than through a
126
+ // PostMessage to a handle that is going away.
127
+ uiThread().window = nullptr;
128
+ uiThread().threadId = std::thread::id{};
129
+ DestroyWindow(window);
130
+ // And whatever was queued and never pumped, which is now nobody's: running
131
+ // it here is what the no-window path would have done with it anyway.
132
+ drainPendingWork();
133
+ }
134
+
119
135
  bool isUiThread() {
120
136
  // With no UI thread installed there is only one thread that matters, so
121
137
  // saying yes is both true and what keeps the tests from marshalling into a
@@ -160,6 +160,13 @@ class RnWin32View {
160
160
  // as transparent black: a view with no background does not paint at all.
161
161
  void setBackgroundColor(float red, float green, float blue, float alpha, bool hasColor);
162
162
  bool hasBackgroundColor() const { return hasBackgroundColor_; }
163
+ // The four components, in the order they were set. Only meaningful when
164
+ // `hasBackgroundColor()`; a view without one has never written them.
165
+ //
166
+ // Read by Win32PeerClip, which has to know whether a view painted over a
167
+ // `<TextInput>` covers it, and that is the alpha. Painting itself does not
168
+ // need the accessor -- it is a member and reads the array directly.
169
+ const float *backgroundColor() const { return backgroundColor_; }
163
170
 
164
171
  void setOpacity(float opacity);
165
172
  float opacity() const { return opacity_; }
@@ -1,31 +1,17 @@
1
1
  #include "Win32AnimationChoreographer.h"
2
2
 
3
- #include <windows.h>
3
+ #include "Win32UiTimer.h"
4
4
 
5
5
  #include <chrono>
6
6
 
7
7
  namespace basalt {
8
8
  namespace {
9
9
 
10
- // Roughly sixty hertz. SetTimer's resolution is the message queue's -- it
11
- // cannot do better than about ten to sixteen milliseconds and it coalesces --
12
- // so this is an upper bound on the frame rate rather than a promise of one.
13
- // See the header for what would be needed to do it properly.
14
- constexpr UINT kFrameIntervalMs = 16;
15
-
16
- // SetTimer's callback carries the timer id and nothing else, so the
17
- // choreographer has to be found from it. One host means one choreographer, and
18
- // a map would be pretending otherwise.
19
- Win32AnimationChoreographer *&currentChoreographer() {
20
- static Win32AnimationChoreographer *instance = nullptr;
21
- return instance;
22
- }
23
-
24
- void CALLBACK onTimer(HWND, UINT, UINT_PTR, DWORD) {
25
- if (auto *choreographer = currentChoreographer()) {
26
- choreographer->onFrame();
27
- }
28
- }
10
+ // Roughly sixty hertz. The delay is a floor rather than a promise -- the frame
11
+ // is posted to the loop and arrives behind whatever the loop is already
12
+ // holding -- so this is an upper bound on the frame rate. See the header for
13
+ // what would be needed to do it properly.
14
+ constexpr unsigned kFrameIntervalMs = 16;
29
15
 
30
16
  } // namespace
31
17
 
@@ -37,28 +23,25 @@ void Win32AnimationChoreographer::resume() {
37
23
  if (running_.exchange(true)) {
38
24
  return;
39
25
  }
40
- currentChoreographer() = this;
41
- // A timer with no window: the callback is delivered as WM_TIMER to the
42
- // thread's queue and dispatched by the loop, which means it lands on the UI
43
- // thread like everything else and needs no synchronisation of its own.
44
- timer_ = static_cast<std::uintptr_t>(SetTimer(nullptr, 0, kFrameIntervalMs, onTimer));
26
+ // Not `SetTimer`. A sixty-hertz WM_TIMER is exactly the arrangement that
27
+ // stops working in an app that repaints continuously, which is to say in
28
+ // every app with an animation worth running. See win32/Win32UiTimer.h.
29
+ timer_ = scheduleUiTimer(kFrameIntervalMs, true, [this] { onFrame(); });
45
30
  }
46
31
 
47
32
  void Win32AnimationChoreographer::pause() {
48
33
  if (!running_.exchange(false)) {
49
34
  return;
50
35
  }
51
- if (timer_ != 0) {
52
- KillTimer(nullptr, static_cast<UINT_PTR>(timer_));
53
- timer_ = 0;
54
- }
36
+ // Before this returns, nothing of ours is still in flight: `cancelUiTimer`
37
+ // waits out a delivery already on a pool thread, which is what makes the
38
+ // `this` captured above safe to destroy afterwards.
39
+ cancelUiTimer(timer_);
40
+ timer_ = 0;
55
41
  }
56
42
 
57
43
  void Win32AnimationChoreographer::detach() {
58
44
  pause();
59
- if (currentChoreographer() == this) {
60
- currentChoreographer() = nullptr;
61
- }
62
45
  }
63
46
 
64
47
  void Win32AnimationChoreographer::onFrame() {
@@ -12,8 +12,8 @@
12
12
  // own thread, because DwmFlush blocks, and a blocked UI thread is worse than a
13
13
  // slightly wrong frame interval.
14
14
  //
15
- // So this is a multimedia timer at sixty hertz, and it is worth being clear
16
- // that this is a placeholder rather than a design. `docs/BACKLOG.md` already
15
+ // So this is a timer at sixty hertz, and it is worth being clear that this is
16
+ // a placeholder rather than a design. `docs/BACKLOG.md` already
17
17
  // records that worklets and Reanimated run on a sixteen-millisecond timer on
18
18
  // *both* other desktops for a related reason -- each platform's display link
19
19
  // belongs to React Native's own choreographer and pauses when React Native has
@@ -52,10 +52,13 @@ class Win32AnimationChoreographer final : public facebook::react::AnimationChore
52
52
 
53
53
  private:
54
54
  std::atomic<bool> running_{false};
55
- // A UINT_PTR from SetTimer, held as an integer so this header needs no
56
- // windows.h -- which would otherwise reach every translation unit that
57
- // constructs a host.
58
- std::uintptr_t timer_{0};
55
+ // A `basalt::UiTimerId` rather than a `SetTimer` handle, and not for
56
+ // tidiness: WM_TIMER ranks below WM_PAINT, so on this timer an app that
57
+ // repaints continuously -- anything with a `<Canvas>` -- stopped getting
58
+ // animation frames at all. See win32/Win32UiTimer.h. Held as a plain
59
+ // integer so this header needs no windows.h, which would otherwise reach
60
+ // every translation unit that constructs a host.
61
+ std::uint64_t timer_{0};
59
62
  };
60
63
 
61
64
  } // namespace basalt
@@ -244,20 +244,6 @@ HCURSOR cursorFromArt(const Art &art, int hotX, int hotY) {
244
244
  return cursor;
245
245
  }
246
246
 
247
- // `%SystemRoot%\Cursors\aero_link.cur`, the shell's own shortcut cursor, which
248
- // is the one of the six Windows ships as a file with a name rather than as an
249
- // ordinal inside a DLL. Null when it is not there, and the caller draws its own.
250
- HCURSOR cursorFromSystemFile(const wchar_t *leaf) {
251
- wchar_t path[MAX_PATH]{};
252
- const UINT length = GetWindowsDirectoryW(path, MAX_PATH);
253
- if (length == 0 || length >= MAX_PATH) {
254
- return nullptr;
255
- }
256
- std::wstring full(path);
257
- full += L"\\Cursors\\";
258
- full += leaf;
259
- return LoadCursorFromFileW(full.c_str());
260
- }
261
247
 
262
248
  HCURSOR drawnCursorFor(const std::string &keyword) {
263
249
  if (keyword == "copy") {
@@ -267,9 +253,19 @@ HCURSOR drawnCursorFor(const std::string &keyword) {
267
253
  return cursorFromArt(contextMenuArt(), 0, 0);
268
254
  }
269
255
  if (keyword == "alias") {
270
- if (HCURSOR shipped = cursorFromSystemFile(L"aero_link.cur")) {
271
- return shipped;
272
- }
256
+ // Drawn, like the other five, and not `%SystemRoot%\Cursors\aero_link.cur`.
257
+ //
258
+ // That file was preferred here on the reasoning that a real name beats an
259
+ // unnamed ordinal inside shell32, which is sound and is still why nothing
260
+ // else reaches into the shell. What it missed is what the file contains:
261
+ // on Windows 11 aero_link.cur is "Link Select", the pointing hand -- the
262
+ // same cursor `pointer` already gets from IDC_HAND. So `alias` and
263
+ // `pointer` came back pixel for pixel identical, and CSS means two
264
+ // different things by them.
265
+ //
266
+ // The art below is the shape `alias` is supposed to be: an arrow with the
267
+ // shortcut badge turning up and to the right. It was already written and
268
+ // never ran, because the file always loaded.
273
269
  return cursorFromArt(aliasArt(), 0, 0);
274
270
  }
275
271
  if (keyword == "cell") {
@@ -15,7 +15,10 @@
15
15
  #include "PlatformServices.h"
16
16
  #include "UIManagerAccess.h"
17
17
 
18
- #ifdef BASALT_HAS_SKIA
18
+ // Either Skia: the 2.x peer in this package or the 3.x one in basalt-skia,
19
+ // which share this header's two functions and are never both linked. See
20
+ // Win32SkiaPeer.h.
21
+ #if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
19
22
  #include "Win32SkiaPeer.h"
20
23
  #endif
21
24
 
@@ -343,7 +346,7 @@ void Win32MountingManager::forgetTag(Tag tag) {
343
346
  labels_.forget(tag);
344
347
  liveRegions_.forget(tag);
345
348
  liveRegionLabels_.erase(tag);
346
- #ifdef BASALT_HAS_SKIA
349
+ #if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
347
350
  // Unregisters the canvas. This is the one moment a view is known to be
348
351
  // finished with, and a canvas left registered is a surface the package will
349
352
  // keep rendering into for a tag nothing will ever draw again.
@@ -360,7 +363,7 @@ void Win32MountingManager::updateView(RnWin32View *view, const ShadowView &shado
360
363
  applyText(view, shadowView);
361
364
  applyImage(view, shadowView);
362
365
  applyAccessibility(view, shadowView);
363
- #ifdef BASALT_HAS_SKIA
366
+ #if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
364
367
  // After applyProps, which is what puts nativeID on the view -- a canvas
365
368
  // registers itself by that id and cannot attach before it is there. The size
366
369
  // it reads comes from the shadow view rather than from the frame