basalt-win32 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/native/CMakeLists.txt +56 -9
- package/native/tests/test_win32_accessibility.cpp +77 -0
- package/native/tests/test_win32_focus.cpp +26 -0
- package/native/tests/test_win32_input.cpp +82 -0
- package/native/tests/test_win32_peer_clip.cpp +266 -0
- package/native/tests/test_win32_run_loop.cpp +122 -0
- package/native/tests/test_win32_textinput.cpp +199 -0
- package/native/tests/test_win32_ui_timer.cpp +195 -0
- package/native/win32/PlatformServicesWin32.cpp +16 -0
- package/native/win32/RnWin32Accessible.cpp +27 -0
- package/native/win32/RnWin32Accessible.h +5 -0
- package/native/win32/RnWin32View.h +18 -0
- package/native/win32/Win32AnimationChoreographer.cpp +15 -32
- package/native/win32/Win32AnimationChoreographer.h +9 -6
- package/native/win32/Win32AppWindow.h +36 -0
- package/native/win32/Win32Cursors.cpp +13 -17
- package/native/win32/Win32DropTarget.cpp +4 -1
- package/native/win32/Win32MenuBar.cpp +3 -1
- package/native/win32/Win32MountingManager.cpp +82 -7
- package/native/win32/Win32MountingManager.h +5 -0
- package/native/win32/Win32PeerClip.cpp +153 -0
- package/native/win32/Win32PeerClip.h +92 -0
- package/native/win32/Win32RunLoopObserver.cpp +39 -9
- package/native/win32/Win32RunLoopObserver.h +41 -2
- package/native/win32/Win32ScrollView.cpp +13 -32
- package/native/win32/Win32ScrollView.h +5 -6
- package/native/win32/Win32SkiaPeer.h +26 -4
- package/native/win32/Win32TextInput.cpp +139 -48
- package/native/win32/Win32TextInput.h +28 -8
- package/native/win32/Win32UiThread.h +11 -0
- package/native/win32/Win32UiTimer.cpp +139 -0
- package/native/win32/Win32UiTimer.h +65 -0
- package/native/win32/Win32WindowControl.cpp +38 -15
- package/native/win32/Win32WindowModule.cpp +8 -13
- package/native/win32/main_win32.cpp +123 -96
- package/package.json +2 -2
|
@@ -1,31 +1,17 @@
|
|
|
1
1
|
#include "Win32AnimationChoreographer.h"
|
|
2
2
|
|
|
3
|
-
#include
|
|
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.
|
|
11
|
-
//
|
|
12
|
-
// so this is an upper bound on the frame rate
|
|
13
|
-
//
|
|
14
|
-
constexpr
|
|
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 *¤tChoreographer() {
|
|
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
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
16
|
-
//
|
|
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
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Which HWND a surface id means.
|
|
2
|
+
//
|
|
3
|
+
// The same seam AppKitAppWindow.h and GtkAppWindow.h describe, written for
|
|
4
|
+
// this toolkit and for the same reason: the registry of open windows is
|
|
5
|
+
// `main_win32.cpp`'s, and everything that resolves one is compiled into the
|
|
6
|
+
// mounting library, which the host links rather than the other way round. So
|
|
7
|
+
// the host installs a lookup at startup and this is what the window module
|
|
8
|
+
// asks.
|
|
9
|
+
//
|
|
10
|
+
// Not installed is the normal case for a test binary: it opens no window, and
|
|
11
|
+
// should not grow a registry to keep compiling. `win32AppWindowFor` falls back
|
|
12
|
+
// to the active window, which is what every caller here did before windows had
|
|
13
|
+
// ids.
|
|
14
|
+
|
|
15
|
+
#pragma once
|
|
16
|
+
|
|
17
|
+
#include <react/renderer/core/ReactPrimitives.h>
|
|
18
|
+
|
|
19
|
+
#include <functional>
|
|
20
|
+
|
|
21
|
+
#include <windows.h>
|
|
22
|
+
|
|
23
|
+
namespace basalt {
|
|
24
|
+
|
|
25
|
+
// The app's own window, by the rule this host has always used: the active
|
|
26
|
+
// window, falling back to the foreground one. UI thread only.
|
|
27
|
+
HWND win32AppWindow();
|
|
28
|
+
|
|
29
|
+
using Win32AppWindowLookup = std::function<HWND(facebook::react::SurfaceId)>;
|
|
30
|
+
void setWin32AppWindowLookup(Win32AppWindowLookup lookup);
|
|
31
|
+
|
|
32
|
+
// The window with this surface id, or the app's own for `kAppWindow` and for
|
|
33
|
+
// an id the lookup does not know. UI thread only.
|
|
34
|
+
HWND win32AppWindowFor(facebook::react::SurfaceId window);
|
|
35
|
+
|
|
36
|
+
} // 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
|
-
|
|
271
|
-
|
|
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") {
|
|
@@ -490,7 +490,10 @@ facebook::react::Tag dropTargetAt(win32::RnWin32View *root,
|
|
|
490
490
|
}
|
|
491
491
|
// Deepest first, which is the rule: a drop target inside another wins.
|
|
492
492
|
for (auto it = path.rbegin(); it != path.rend(); ++it) {
|
|
493
|
-
|
|
493
|
+
// The `nativeID` marker and the view's own declared kinds, which is how a
|
|
494
|
+
// <TextInput acceptDragAndDropTypes> joins this walk without an id.
|
|
495
|
+
const std::uint16_t wanted =
|
|
496
|
+
static_cast<std::uint16_t>(dropAcceptsFrom((*it)->nativeId()) | (*it)->dropAccepts());
|
|
494
497
|
if (wanted != DropAcceptsNone && (wanted & accepts) != 0) {
|
|
495
498
|
return static_cast<facebook::react::Tag>((*it)->tag());
|
|
496
499
|
}
|
|
@@ -342,7 +342,9 @@ bool handleMenuCommand(unsigned int command) {
|
|
|
342
342
|
PostMessageW(window, WM_CLOSE, 0, 0);
|
|
343
343
|
return true;
|
|
344
344
|
case kRoleBase + 12:
|
|
345
|
-
|
|
345
|
+
// The app's own window: a menu role acts on the window the menu bar
|
|
346
|
+
// belongs to, which is that one. See core/WindowControl.h.
|
|
347
|
+
setWindowFullScreen(kAppWindow, !windowBounds(kAppWindow).fullScreen);
|
|
346
348
|
return true;
|
|
347
349
|
default:
|
|
348
350
|
return false;
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
#include "AccessibilityRole.h"
|
|
2
|
+
#include "KeyboardFocusable.h"
|
|
3
|
+
#include "ImagePlaceholder.h"
|
|
1
4
|
#include "Win32MountingManager.h"
|
|
2
5
|
|
|
3
6
|
#include "BackgroundLayers.h"
|
|
@@ -15,7 +18,10 @@
|
|
|
15
18
|
#include "PlatformServices.h"
|
|
16
19
|
#include "UIManagerAccess.h"
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
// Either Skia: the 2.x peer in this package or the 3.x one in basalt-skia,
|
|
22
|
+
// which share this header's two functions and are never both linked. See
|
|
23
|
+
// Win32SkiaPeer.h.
|
|
24
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
19
25
|
#include "Win32SkiaPeer.h"
|
|
20
26
|
#endif
|
|
21
27
|
|
|
@@ -339,11 +345,13 @@ void Win32MountingManager::forgetTag(Tag tag) {
|
|
|
339
345
|
scrollViews_.remove(tag);
|
|
340
346
|
textInputs_.remove(tag);
|
|
341
347
|
imageUris_.erase(tag);
|
|
348
|
+
imagePlaceholders_.erase(tag);
|
|
349
|
+
imageLoaded_.erase(tag);
|
|
342
350
|
switchValues_.erase(tag);
|
|
343
351
|
labels_.forget(tag);
|
|
344
352
|
liveRegions_.forget(tag);
|
|
345
353
|
liveRegionLabels_.erase(tag);
|
|
346
|
-
#
|
|
354
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
347
355
|
// Unregisters the canvas. This is the one moment a view is known to be
|
|
348
356
|
// finished with, and a canvas left registered is a surface the package will
|
|
349
357
|
// keep rendering into for a tag nothing will ever draw again.
|
|
@@ -360,7 +368,7 @@ void Win32MountingManager::updateView(RnWin32View *view, const ShadowView &shado
|
|
|
360
368
|
applyText(view, shadowView);
|
|
361
369
|
applyImage(view, shadowView);
|
|
362
370
|
applyAccessibility(view, shadowView);
|
|
363
|
-
#
|
|
371
|
+
#if defined(BASALT_HAS_SKIA) || BASALT_HAS_SKIA_GRAPHITE
|
|
364
372
|
// After applyProps, which is what puts nativeID on the view -- a canvas
|
|
365
373
|
// registers itself by that id and cannot attach before it is there. The size
|
|
366
374
|
// it reads comes from the shadow view rather than from the frame
|
|
@@ -869,6 +877,9 @@ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shado
|
|
|
869
877
|
const facebook::react::ImageSource source =
|
|
870
878
|
props->sources.empty() ? facebook::react::ImageSource{} : props->sources.front();
|
|
871
879
|
const std::string uri = source.uri;
|
|
880
|
+
// `defaultSource`, drawn until the source lands and kept if it never does.
|
|
881
|
+
// See core/ImagePlaceholder.h for which of the two props this is.
|
|
882
|
+
const std::string placeholder = basalt::imagePlaceholderUri(*props);
|
|
872
883
|
|
|
873
884
|
// `tintColor`, applied before the load rather than in its callback: the tint
|
|
874
885
|
// is a prop and the image is a loader's answer, and either can arrive first.
|
|
@@ -911,6 +922,8 @@ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shado
|
|
|
911
922
|
return;
|
|
912
923
|
}
|
|
913
924
|
imageUris_[shadowView.tag] = uri;
|
|
925
|
+
imagePlaceholders_[shadowView.tag] = placeholder;
|
|
926
|
+
imageLoaded_.erase(shadowView.tag);
|
|
914
927
|
|
|
915
928
|
if (uri.empty()) {
|
|
916
929
|
view->setImage(nullptr, fit);
|
|
@@ -923,6 +936,38 @@ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shado
|
|
|
923
936
|
// the change did not take.
|
|
924
937
|
view->setImage(nullptr, fit);
|
|
925
938
|
|
|
939
|
+
// Then the placeholder, if the app named one, so the blank box above is as
|
|
940
|
+
// short as the loader is slow. A bundled asset answers from the cache on
|
|
941
|
+
// this thread, which makes it the same frame.
|
|
942
|
+
//
|
|
943
|
+
// Guarded on the real image not having arrived: both loads are asynchronous
|
|
944
|
+
// and a slow placeholder must not paint over the image it stood in for.
|
|
945
|
+
if (!placeholder.empty() && uri != placeholder) {
|
|
946
|
+
const Tag placeholderTag = shadowView.tag;
|
|
947
|
+
imageLoader_->load(
|
|
948
|
+
placeholder,
|
|
949
|
+
[this, placeholderTag, fit, placeholder](std::shared_ptr<win32::RnWin32Image> image,
|
|
950
|
+
const std::string &) {
|
|
951
|
+
if (image == nullptr || imageLoaded_.count(placeholderTag) != 0) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
const auto still = imagePlaceholders_.find(placeholderTag);
|
|
955
|
+
if (still == imagePlaceholders_.end() || still->second != placeholder) {
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
RnWin32View *target = viewForTag(placeholderTag);
|
|
959
|
+
if (target == nullptr) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
target->setImage(image, fit);
|
|
963
|
+
// The same reason the load callback below calls it: the bytes
|
|
964
|
+
// arrived on their own clock and only the host has the HWND.
|
|
965
|
+
if (onDidMount_) {
|
|
966
|
+
onDidMount_();
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
|
|
926
971
|
// `<Image>`'s load events, which this host emitted none of until 2026-10-10:
|
|
927
972
|
// `onLoadStart`, `onProgress`, `onLoad`, `onError` and `onLoadEnd` all
|
|
928
973
|
// reached JavaScript on the other two hosts and nothing here ever dispatched
|
|
@@ -984,7 +1029,20 @@ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shado
|
|
|
984
1029
|
const bool stillWanted =
|
|
985
1030
|
target != nullptr && current != imageUris_.end() && current->second == uri;
|
|
986
1031
|
if (stillWanted) {
|
|
987
|
-
|
|
1032
|
+
// A failed load leaves the placeholder where it is rather than
|
|
1033
|
+
// clearing to nothing, which is `RCTImageComponentView`'s behaviour:
|
|
1034
|
+
// a broken remote image shows what the app chose to show instead of
|
|
1035
|
+
// an empty box.
|
|
1036
|
+
const auto standing = imagePlaceholders_.find(tag);
|
|
1037
|
+
const bool keepPlaceholder = image == nullptr &&
|
|
1038
|
+
standing != imagePlaceholders_.end() &&
|
|
1039
|
+
!standing->second.empty();
|
|
1040
|
+
if (image != nullptr) {
|
|
1041
|
+
imageLoaded_.insert(tag);
|
|
1042
|
+
}
|
|
1043
|
+
if (!keepPlaceholder) {
|
|
1044
|
+
target->setImage(image, fit);
|
|
1045
|
+
}
|
|
988
1046
|
applyAnimation(target, uri);
|
|
989
1047
|
// The screen is stale and no transaction is coming: the bytes arrived
|
|
990
1048
|
// on their own clock, after the mount that asked for them. On GTK and
|
|
@@ -1041,8 +1099,22 @@ win32::RnAccessibleFlag toFlag(bool value) {
|
|
|
1041
1099
|
std::string effectiveRole(const ShadowView &shadowView) {
|
|
1042
1100
|
if (const auto props =
|
|
1043
1101
|
std::dynamic_pointer_cast<const facebook::react::AccessibilityProps>(shadowView.props)) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1102
|
+
const std::string asked = basalt::accessibilityRoleName(*props);
|
|
1103
|
+
if (!asked.empty()) {
|
|
1104
|
+
return asked;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
// Then the paragraph's own fragments, which is where a nested <Text>'s role
|
|
1108
|
+
// lands: `TextAttributes` carries one per run, and a paragraph every fragment
|
|
1109
|
+
// of which agrees *is* that role. Before the inference below, so a paragraph
|
|
1110
|
+
// whose runs all say "header" is a header rather than generic text. See
|
|
1111
|
+
// core/AccessibilityRole.h for why agreement and not the first one.
|
|
1112
|
+
if (const auto state =
|
|
1113
|
+
std::dynamic_pointer_cast<const facebook::react::ConcreteState<facebook::react::ParagraphState>>(
|
|
1114
|
+
shadowView.state)) {
|
|
1115
|
+
const std::string fragments = basalt::fragmentRoleName(state->getData().attributedString);
|
|
1116
|
+
if (!fragments.empty()) {
|
|
1117
|
+
return fragments;
|
|
1046
1118
|
}
|
|
1047
1119
|
}
|
|
1048
1120
|
if (shadowView.componentName != nullptr) {
|
|
@@ -1082,6 +1154,9 @@ void Win32MountingManager::applyAccessibility(RnWin32View *view, const ShadowVie
|
|
|
1082
1154
|
info.testId = props->testId;
|
|
1083
1155
|
// `accessibilityViewIsModal`, which UIA calls a dialog.
|
|
1084
1156
|
info.modal = props->accessibilityViewIsModal;
|
|
1157
|
+
// `accessibilityLanguage`, as the BCP-47 tag the app wrote. The conversion
|
|
1158
|
+
// to the LCID UIA wants happens where the culture property is answered.
|
|
1159
|
+
info.language = props->accessibilityLanguage;
|
|
1085
1160
|
|
|
1086
1161
|
const auto &state = props->accessibilityState;
|
|
1087
1162
|
if (state.has_value()) {
|
|
@@ -1137,7 +1212,7 @@ void Win32MountingManager::applyAccessibility(RnWin32View *view, const ShadowVie
|
|
|
1137
1212
|
// that cannot be operated rather than stopping on one that does nothing.
|
|
1138
1213
|
const basalt::ControlState control = basalt::controlStateOf(shadowView);
|
|
1139
1214
|
const bool isControl = control.kind == basalt::ControlKind::Switch && !control.disabled;
|
|
1140
|
-
view->setFocusable((props
|
|
1215
|
+
view->setFocusable(basalt::keyboardFocusable(*props, isControl));
|
|
1141
1216
|
}
|
|
1142
1217
|
|
|
1143
1218
|
void Win32MountingManager::applyLayoutMetrics(RnWin32View *view, const ShadowView &shadowView) {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
#include <functional>
|
|
35
35
|
#include <string>
|
|
36
36
|
#include <unordered_map>
|
|
37
|
+
#include <unordered_set>
|
|
37
38
|
|
|
38
39
|
namespace basalt {
|
|
39
40
|
|
|
@@ -190,6 +191,10 @@ class Win32MountingManager final : public facebook::react::IMountingManager,
|
|
|
190
191
|
// changed only layout does not restart the load and make the image flicker
|
|
191
192
|
// whenever its parent resizes. Both other platforms keep the same map.
|
|
192
193
|
std::unordered_map<facebook::react::Tag, std::string> imageUris_;
|
|
194
|
+
// `defaultSource` per view, and which views have their real image. See
|
|
195
|
+
// core/ImagePlaceholder.h.
|
|
196
|
+
std::unordered_map<facebook::react::Tag, std::string> imagePlaceholders_;
|
|
197
|
+
std::unordered_set<facebook::react::Tag> imageLoaded_;
|
|
193
198
|
|
|
194
199
|
// Scroll offsets, the unthrottled state write-back, the wheel and the
|
|
195
200
|
// commands. Constructed with a lookup into this object's own emitter registry
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#include "Win32PeerClip.h"
|
|
2
|
+
|
|
3
|
+
#include "RnWin32View.h"
|
|
4
|
+
|
|
5
|
+
#include <algorithm>
|
|
6
|
+
#include <cmath>
|
|
7
|
+
|
|
8
|
+
namespace basalt::win32 {
|
|
9
|
+
|
|
10
|
+
namespace {
|
|
11
|
+
|
|
12
|
+
RECT boxOf(double x, double y, double width, double height) {
|
|
13
|
+
RECT rect{};
|
|
14
|
+
rect.left = static_cast<LONG>(std::lround(x));
|
|
15
|
+
rect.top = static_cast<LONG>(std::lround(y));
|
|
16
|
+
rect.right = rect.left + static_cast<LONG>(std::lround(width));
|
|
17
|
+
rect.bottom = rect.top + static_cast<LONG>(std::lround(height));
|
|
18
|
+
return rect;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
bool isEmpty(const RECT &rect) {
|
|
22
|
+
return rect.right <= rect.left || rect.bottom <= rect.top;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
RECT intersect(const RECT &a, const RECT &b) {
|
|
26
|
+
RECT out{};
|
|
27
|
+
out.left = std::max(a.left, b.left);
|
|
28
|
+
out.top = std::max(a.top, b.top);
|
|
29
|
+
out.right = std::min(a.right, b.right);
|
|
30
|
+
out.bottom = std::min(a.bottom, b.bottom);
|
|
31
|
+
if (isEmpty(out)) {
|
|
32
|
+
return RECT{0, 0, 0, 0};
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Whether this view puts something over the whole of its own box.
|
|
38
|
+
//
|
|
39
|
+
// A background colour with any alpha at all, and nothing else -- see the
|
|
40
|
+
// header for why the bar is "not transparent" rather than "opaque", and why a
|
|
41
|
+
// transformed view is left alone instead of being approximated by its bounding
|
|
42
|
+
// box.
|
|
43
|
+
bool covers(const RnWin32View *view) {
|
|
44
|
+
if (!view->hasBackgroundColor() || view->hasTransform()) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (view->opacity() <= 0.0f) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return view->backgroundColor()[3] > 0.0f;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
struct Walk {
|
|
54
|
+
const RnWin32View *target{nullptr};
|
|
55
|
+
PeerClip *out{nullptr};
|
|
56
|
+
// Set when the target has been reached, which is what divides the tree into
|
|
57
|
+
// "painted before it", which cannot cover it, and "painted after it", which
|
|
58
|
+
// can. One flag is enough because painting order is a single sequence.
|
|
59
|
+
bool seen{false};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// `origin` is where this view's parent puts its children, already scrolled.
|
|
63
|
+
void visit(RnWin32View *view, double originX, double originY, const RECT &clip, Walk &walk) {
|
|
64
|
+
if (view == nullptr || view->hidden()) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const double x = originX + view->frame().x;
|
|
69
|
+
const double y = originY + view->frame().y;
|
|
70
|
+
const RECT box = boxOf(x, y, view->frame().width, view->frame().height);
|
|
71
|
+
|
|
72
|
+
if (view == walk.target) {
|
|
73
|
+
const RECT visible = intersect(box, clip);
|
|
74
|
+
if (isEmpty(visible)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
walk.out->visible = true;
|
|
78
|
+
walk.out->bounds = box;
|
|
79
|
+
walk.out->clip = visible;
|
|
80
|
+
// Its own subtree is not walked: a child of a `<TextInput>` is inside the
|
|
81
|
+
// field rather than over it, and the field has none in any real tree.
|
|
82
|
+
walk.seen = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (walk.seen && covers(view)) {
|
|
87
|
+
const RECT over = intersect(box, clip);
|
|
88
|
+
if (!isEmpty(over)) {
|
|
89
|
+
walk.out->occluders.push_back(over);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
RECT childClip = clip;
|
|
94
|
+
if (view->clipsChildren()) {
|
|
95
|
+
childClip = intersect(clip, box);
|
|
96
|
+
if (isEmpty(childClip)) {
|
|
97
|
+
// Nothing inside can be seen, and nothing inside can cover anything
|
|
98
|
+
// either -- but the target may still be in there, and if it is then it is
|
|
99
|
+
// off screen, which is what not finding it already means.
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const double childX = x - view->scrollX();
|
|
105
|
+
const double childY = y - view->scrollY();
|
|
106
|
+
for (RnWin32View *child : view->childrenInPaintOrder()) {
|
|
107
|
+
visit(child, childX, childY, childClip, walk);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} // namespace
|
|
112
|
+
|
|
113
|
+
PeerClip peerClip(RnWin32View *view, RnWin32View *root) {
|
|
114
|
+
PeerClip result{};
|
|
115
|
+
if (view == nullptr || root == nullptr) {
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The root's own frame is not part of these coordinates: the surface root is
|
|
120
|
+
// the client area, and a peer's position is measured from its top-left. So
|
|
121
|
+
// the walk starts inside it rather than at it, which also means a `view` that
|
|
122
|
+
// *is* the root is not a field and comes back invisible.
|
|
123
|
+
if (root->hidden()) {
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Unbounded unless the root says otherwise, which is the rule every other
|
|
128
|
+
// ancestor is under. Clipping to the client area regardless would be defen-
|
|
129
|
+
// sible and is not done here: a surface that has never been laid out has a
|
|
130
|
+
// zero frame, and "every field is invisible" is a worse answer than "a field
|
|
131
|
+
// outside the window is placed outside the window", which USER32 handles.
|
|
132
|
+
constexpr LONG kUnbounded = 1 << 28;
|
|
133
|
+
RECT clip{-kUnbounded, -kUnbounded, kUnbounded, kUnbounded};
|
|
134
|
+
if (root->clipsChildren()) {
|
|
135
|
+
clip = boxOf(0.0, 0.0, root->frame().width, root->frame().height);
|
|
136
|
+
if (isEmpty(clip)) {
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
Walk walk{view, &result, false};
|
|
141
|
+
const double childX = -static_cast<double>(root->scrollX());
|
|
142
|
+
const double childY = -static_cast<double>(root->scrollY());
|
|
143
|
+
for (RnWin32View *child : root->childrenInPaintOrder()) {
|
|
144
|
+
visit(child, childX, childY, clip, walk);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!result.visible) {
|
|
148
|
+
return PeerClip{};
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
} // namespace basalt::win32
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Where a child-window peer is allowed to paint.
|
|
2
|
+
//
|
|
3
|
+
// A `<TextInput>` on this host is a real EDIT control, so it is a child window
|
|
4
|
+
// rather than something Direct2D draws. That buys the IME, the clipboard and
|
|
5
|
+
// every binding a Windows user expects, and it costs the one thing every other
|
|
6
|
+
// view gets for free: a place in the painting order. A child window is drawn by
|
|
7
|
+
// USER32 after the parent's client area, so it sits above everything the React
|
|
8
|
+
// tree drew, whatever the tree says. Two things follow, and a person sees both:
|
|
9
|
+
//
|
|
10
|
+
// - A field in a `<ScrollView>` does not clip to it. Scroll the list until
|
|
11
|
+
// the field is half out and the half that should be gone goes on being
|
|
12
|
+
// drawn over the list's neighbour.
|
|
13
|
+
// - A field cannot be covered. Open a modal over a pane that has one and the
|
|
14
|
+
// field floats on top of the modal, at full brightness, still editable.
|
|
15
|
+
//
|
|
16
|
+
// Both are the same missing fact -- which pixels of the field are still the
|
|
17
|
+
// field's -- and Windows has somewhere to put it: `SetWindowRgn`. A window with
|
|
18
|
+
// a region is displayed only inside it, and clicks outside it fall through to
|
|
19
|
+
// the parent. So the job is to work the region out from the view tree, which is
|
|
20
|
+
// what this file is: pure geometry over `RnWin32View`, with no window, no
|
|
21
|
+
// control and no React Native in it, so that it can be tested with none of
|
|
22
|
+
// them.
|
|
23
|
+
//
|
|
24
|
+
// ## What counts as covering it
|
|
25
|
+
//
|
|
26
|
+
// Clipping is exact: a clipping ancestor's box is a rectangle and the
|
|
27
|
+
// intersection of rectangles is the answer.
|
|
28
|
+
//
|
|
29
|
+
// Occlusion is not, and cannot be. The right behaviour for a field under
|
|
30
|
+
// kino's modal scrim -- `#000000A0` over the whole window -- is for the field
|
|
31
|
+
// to be *dimmed*, and a child window cannot be dimmed by something painted into
|
|
32
|
+
// its parent: the two are composited by USER32, in that order, with no blend to
|
|
33
|
+
// take part in. The choice is therefore between showing the field undimmed and
|
|
34
|
+
// hiding it, and hiding it is the better wrong answer: an undimmed field over a
|
|
35
|
+
// modal is the bug people report, while a field that goes quiet under a scrim
|
|
36
|
+
// reads as part of the modal.
|
|
37
|
+
//
|
|
38
|
+
// So anything painted after the field that is not fully transparent counts as
|
|
39
|
+
// covering it. That is deliberately more eager than "opaque": a 63%-black
|
|
40
|
+
// backdrop blends to something no child window can match, so there is nothing
|
|
41
|
+
// to be gained by letting it show through at 100%.
|
|
42
|
+
//
|
|
43
|
+
// What does *not* count: a view with no background at all, which is every
|
|
44
|
+
// layout wrapper in a real tree and the reason this rule produces no false
|
|
45
|
+
// positives in practice, and a transformed view, whose box is not a rectangle
|
|
46
|
+
// in these coordinates and which is therefore left alone rather than guessed
|
|
47
|
+
// at. A `<Text>` drawn over a field is also left alone -- glyphs are not a box.
|
|
48
|
+
//
|
|
49
|
+
// ## Transforms
|
|
50
|
+
//
|
|
51
|
+
// Ignored throughout, as `Win32TextInputManager` already ignores them when it
|
|
52
|
+
// places the peer: a window cannot be rotated, so a transformed field's peer
|
|
53
|
+
// sits where the untransformed one would, and clipping it to a rotated
|
|
54
|
+
// ancestor's bounding box would only move the error around.
|
|
55
|
+
|
|
56
|
+
#pragma once
|
|
57
|
+
|
|
58
|
+
#include <windows.h>
|
|
59
|
+
|
|
60
|
+
#include <vector>
|
|
61
|
+
|
|
62
|
+
namespace basalt::win32 {
|
|
63
|
+
|
|
64
|
+
class RnWin32View;
|
|
65
|
+
|
|
66
|
+
// Where `view`'s peer may paint, all in the surface root's coordinates.
|
|
67
|
+
struct PeerClip {
|
|
68
|
+
// False when the view is not on screen at all: hidden, detached, or scrolled
|
|
69
|
+
// entirely out of a clipping ancestor. Nothing else in the struct means
|
|
70
|
+
// anything then.
|
|
71
|
+
bool visible{false};
|
|
72
|
+
|
|
73
|
+
// The view's own box, unclipped. This is where the control goes -- a control
|
|
74
|
+
// narrowed to its visible part would lay its text out in the wrong width.
|
|
75
|
+
RECT bounds{};
|
|
76
|
+
|
|
77
|
+
// `bounds` narrowed by every clipping ancestor. Never empty when `visible`.
|
|
78
|
+
RECT clip{};
|
|
79
|
+
|
|
80
|
+
// Boxes painted after the view that cover part of it, already narrowed to
|
|
81
|
+
// their own clipping ancestors. Each is taken out of the region.
|
|
82
|
+
std::vector<RECT> occluders{};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Walks the tree from `root` once, in painting order, and answers for `view`.
|
|
86
|
+
//
|
|
87
|
+
// `root` is the surface root, whose coordinates are the window's client
|
|
88
|
+
// coordinates on this host. A view that is not under `root` comes back
|
|
89
|
+
// invisible, which is what a peer between a Remove and its Delete looks like.
|
|
90
|
+
PeerClip peerClip(RnWin32View *view, RnWin32View *root);
|
|
91
|
+
|
|
92
|
+
} // namespace basalt::win32
|