basalt-win32 0.2.2 → 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/tests/test_win32_accessibility.cpp +77 -0
- package/native/tests/test_win32_focus.cpp +26 -0
- package/native/tests/test_win32_textinput.cpp +91 -0
- package/native/win32/RnWin32Accessible.cpp +27 -0
- package/native/win32/RnWin32Accessible.h +5 -0
- package/native/win32/RnWin32View.h +11 -0
- package/native/win32/Win32AppWindow.h +36 -0
- package/native/win32/Win32DropTarget.cpp +4 -1
- package/native/win32/Win32MenuBar.cpp +3 -1
- package/native/win32/Win32MountingManager.cpp +76 -4
- package/native/win32/Win32MountingManager.h +5 -0
- package/native/win32/Win32TextInput.cpp +33 -2
- package/native/win32/Win32TextInput.h +5 -0
- package/native/win32/Win32WindowControl.cpp +38 -15
- package/native/win32/Win32WindowModule.cpp +8 -13
- package/native/win32/main_win32.cpp +22 -5
- package/package.json +2 -2
|
@@ -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);
|
|
@@ -179,6 +179,9 @@ struct FieldOptions {
|
|
|
179
179
|
unsigned int placeholderTextColor{0};
|
|
180
180
|
unsigned int selectionColor{0};
|
|
181
181
|
unsigned int cursorColor{0};
|
|
182
|
+
// `scrollEnabled`, which defaults to true the way the trait does. Sent only
|
|
183
|
+
// when false so the other tests' raw props stay as they were.
|
|
184
|
+
bool scrollEnabled{true};
|
|
182
185
|
};
|
|
183
186
|
|
|
184
187
|
ShadowView makeField(Tag tag,
|
|
@@ -196,6 +199,9 @@ ShadowView makeField(Tag tag,
|
|
|
196
199
|
"clearTextOnFocus", options.clearTextOnFocus)(
|
|
197
200
|
"selectTextOnFocus", options.selectTextOnFocus)("caretHidden", options.caretHidden)(
|
|
198
201
|
"contextMenuHidden", options.contextMenuHidden);
|
|
202
|
+
if (!options.scrollEnabled) {
|
|
203
|
+
raw["scrollEnabled"] = false;
|
|
204
|
+
}
|
|
199
205
|
if (!options.submitBehavior.empty()) {
|
|
200
206
|
raw["submitBehavior"] = options.submitBehavior;
|
|
201
207
|
}
|
|
@@ -889,6 +895,91 @@ TEST(win32_a_multiline_field_is_a_multiline_control) {
|
|
|
889
895
|
manager->destroySurfaceRoot(kSurfaceId);
|
|
890
896
|
}
|
|
891
897
|
|
|
898
|
+
// `scrollEnabled: false`, which is that vertical scroll withheld. A
|
|
899
|
+
// creation-time style bit like `ES_MULTILINE` beside it, so the assertions are
|
|
900
|
+
// the same shape.
|
|
901
|
+
TEST(win32_a_field_that_disables_scrolling_has_no_vertical_scroll) {
|
|
902
|
+
auto manager = makeManager();
|
|
903
|
+
RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
|
|
904
|
+
root->setFrame(0, 0, 800, 600);
|
|
905
|
+
mount(*manager,
|
|
906
|
+
kSurfaceId,
|
|
907
|
+
makeField(10, 20, 30, 200, 120,
|
|
908
|
+
{.text = "", .multiline = true, .scrollEnabled = false}));
|
|
909
|
+
manager->syncTextInputBounds(root);
|
|
910
|
+
|
|
911
|
+
HWND control = peerOf(manager->viewForTag(10));
|
|
912
|
+
EXPECT(control != nullptr);
|
|
913
|
+
if (control == nullptr) {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const LONG_PTR style = GetWindowLongPtr(control, GWL_STYLE);
|
|
917
|
+
// Still a multiline control: the prop is about scrolling and nothing else.
|
|
918
|
+
EXPECT((style & ES_MULTILINE) != 0);
|
|
919
|
+
EXPECT((style & ES_AUTOVSCROLL) == 0);
|
|
920
|
+
// And it still takes the text, which is the half that makes this a display
|
|
921
|
+
// decision rather than a limit.
|
|
922
|
+
type(control, L"a\rb");
|
|
923
|
+
EXPECT_EQ(textOf(control), std::string("a\r\nb"));
|
|
924
|
+
|
|
925
|
+
manager->destroySurfaceRoot(kSurfaceId);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Only for a multiline field. A single line scrolls sideways through
|
|
929
|
+
// `ES_AUTOHSCROLL`, which React Native's prop says nothing about, so a field
|
|
930
|
+
// that sets it there is unchanged -- and in particular does not lose the
|
|
931
|
+
// horizontal scroll that lets the caret leave the right-hand edge.
|
|
932
|
+
TEST(win32_a_single_line_field_ignores_the_scroll_prop) {
|
|
933
|
+
auto manager = makeManager();
|
|
934
|
+
RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
|
|
935
|
+
root->setFrame(0, 0, 800, 600);
|
|
936
|
+
mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44, {.scrollEnabled = false}));
|
|
937
|
+
manager->syncTextInputBounds(root);
|
|
938
|
+
|
|
939
|
+
HWND control = peerOf(manager->viewForTag(10));
|
|
940
|
+
EXPECT(control != nullptr);
|
|
941
|
+
if (control == nullptr) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
EXPECT((GetWindowLongPtr(control, GWL_STYLE) & ES_AUTOHSCROLL) != 0);
|
|
945
|
+
|
|
946
|
+
manager->destroySurfaceRoot(kSurfaceId);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// The style cannot be added to a live control, so a field that changes the prop
|
|
950
|
+
// has its peer rebuilt -- the same rule `multiline` follows, and the text comes
|
|
951
|
+
// across for the same reason.
|
|
952
|
+
TEST(win32_changing_scroll_enabled_rebuilds_the_peer) {
|
|
953
|
+
auto manager = makeManager();
|
|
954
|
+
RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
|
|
955
|
+
root->setFrame(0, 0, 800, 600);
|
|
956
|
+
const ShadowView scrolling =
|
|
957
|
+
makeField(10, 20, 30, 200, 120, {.text = "wrapped", .multiline = true});
|
|
958
|
+
mount(*manager, kSurfaceId, scrolling);
|
|
959
|
+
manager->syncTextInputBounds(root);
|
|
960
|
+
|
|
961
|
+
HWND before = peerOf(manager->viewForTag(10));
|
|
962
|
+
EXPECT(before != nullptr);
|
|
963
|
+
EXPECT(before != nullptr && (GetWindowLongPtr(before, GWL_STYLE) & ES_AUTOVSCROLL) != 0);
|
|
964
|
+
|
|
965
|
+
update(*manager,
|
|
966
|
+
scrolling,
|
|
967
|
+
makeField(10, 20, 30, 200, 120,
|
|
968
|
+
{.text = "wrapped", .multiline = true, .scrollEnabled = false}));
|
|
969
|
+
manager->syncTextInputBounds(root);
|
|
970
|
+
|
|
971
|
+
HWND after = peerOf(manager->viewForTag(10));
|
|
972
|
+
EXPECT(after != nullptr);
|
|
973
|
+
if (after == nullptr) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
EXPECT(after != before);
|
|
977
|
+
EXPECT((GetWindowLongPtr(after, GWL_STYLE) & ES_AUTOVSCROLL) == 0);
|
|
978
|
+
EXPECT_EQ(textOf(after), std::string("wrapped"));
|
|
979
|
+
|
|
980
|
+
manager->destroySurfaceRoot(kSurfaceId);
|
|
981
|
+
}
|
|
982
|
+
|
|
892
983
|
// A single-line field still swallows Enter, which is the other half of the same
|
|
893
984
|
// rule and the test that was here before this prop existed.
|
|
894
985
|
TEST(win32_a_single_line_field_is_not_multiline) {
|
|
@@ -165,6 +165,33 @@ class AccessibleProvider final : public IRawElementProviderSimple {
|
|
|
165
165
|
}
|
|
166
166
|
return S_OK;
|
|
167
167
|
|
|
168
|
+
case UIA_CulturePropertyId: {
|
|
169
|
+
// React Native's `accessibilityLanguage`, which UIA takes as an LCID
|
|
170
|
+
// and not as the BCP-47 tag every other platform speaks: 1036 where
|
|
171
|
+
// the app wrote "fr-FR". `LocaleNameToLCID` is the documented
|
|
172
|
+
// conversion and the reason this property can be answered at all.
|
|
173
|
+
//
|
|
174
|
+
// `LOCALE_ALLOW_NEUTRAL_NAMES` so a tag with no region -- "fr", which
|
|
175
|
+
// is what an app writes when it means the language and not a country
|
|
176
|
+
// -- converts rather than failing. Without it the function rejects
|
|
177
|
+
// every neutral name, which is a large share of what apps write.
|
|
178
|
+
//
|
|
179
|
+
// Left unset when the tag does not convert, which is the honest answer
|
|
180
|
+
// for a locale this Windows does not know: zero is a valid LCID
|
|
181
|
+
// meaning the system default, so returning the failure would claim the
|
|
182
|
+
// app asked for the system language when it asked for something else.
|
|
183
|
+
if (info_.language.empty()) {
|
|
184
|
+
return S_OK;
|
|
185
|
+
}
|
|
186
|
+
const LCID lcid =
|
|
187
|
+
LocaleNameToLCID(widen(info_.language).c_str(), LOCALE_ALLOW_NEUTRAL_NAMES);
|
|
188
|
+
if (lcid != 0) {
|
|
189
|
+
value->vt = VT_I4;
|
|
190
|
+
value->lVal = static_cast<long>(lcid);
|
|
191
|
+
}
|
|
192
|
+
return S_OK;
|
|
193
|
+
}
|
|
194
|
+
|
|
168
195
|
case UIA_HelpTextPropertyId:
|
|
169
196
|
// React Native's `accessibilityHint` is "what happens if you do this",
|
|
170
197
|
// which is what UIA calls help text. AppKit puts it in the
|
|
@@ -80,6 +80,11 @@ struct RnAccessibleInfo {
|
|
|
80
80
|
// calls an automation id. Not a label -- nothing reads it aloud -- and not
|
|
81
81
|
// `nativeID`, which is how an app names a view to itself.
|
|
82
82
|
std::string testId;
|
|
83
|
+
// `accessibilityLanguage`, as React Native gave it: a BCP-47 tag such as
|
|
84
|
+
// "fr-CA". UIA wants a culture *identifier* rather than a tag, so the
|
|
85
|
+
// conversion happens where the property is answered and not here -- the tag
|
|
86
|
+
// is what the app wrote and what the other hosts publish verbatim.
|
|
87
|
+
std::string language;
|
|
83
88
|
// `accessibilityViewIsModal`. UIA calls this a dialog: a client that honours
|
|
84
89
|
// `IsDialog` stops offering what is behind the element, which is the same
|
|
85
90
|
// behaviour ARIA's `aria-modal` and AppKit's `accessibilityModal` ask for.
|
|
@@ -431,6 +431,16 @@ class RnWin32View {
|
|
|
431
431
|
void setNativeId(std::string nativeId) { nativeId_ = std::move(nativeId); }
|
|
432
432
|
const std::string &nativeId() const { return nativeId_; }
|
|
433
433
|
|
|
434
|
+
// What this view will take on a drop, over and above what its `nativeID`
|
|
435
|
+
// says: a `DropAccepts` mask from core/DragAndDrop.h.
|
|
436
|
+
//
|
|
437
|
+
// A second source because a `<TextInput>` declares it with a real prop,
|
|
438
|
+
// `acceptDragAndDropTypes`, where a plain `<View>` has only the `nativeID`
|
|
439
|
+
// marker this platform invented. The drop walk reads both, so a field can
|
|
440
|
+
// accept a drop without being given a magic id.
|
|
441
|
+
void setDropAccepts(std::uint16_t accepts) { dropAccepts_ = accepts; }
|
|
442
|
+
std::uint16_t dropAccepts() const { return dropAccepts_; }
|
|
443
|
+
|
|
434
444
|
// --- Content ---------------------------------------------------------------
|
|
435
445
|
|
|
436
446
|
// The paragraph this view draws, or null for a view that draws none.
|
|
@@ -805,6 +815,7 @@ class RnWin32View {
|
|
|
805
815
|
bool hasBorders_ = false;
|
|
806
816
|
HWND editablePeer_ = nullptr;
|
|
807
817
|
std::string nativeId_;
|
|
818
|
+
std::uint16_t dropAccepts_{0};
|
|
808
819
|
int zIndex_ = 0;
|
|
809
820
|
PointerEvents pointerEvents_ = PointerEvents::Auto;
|
|
810
821
|
float hitSlop_[4] = {0.0f, 0.0f, 0.0f, 0.0f};
|
|
@@ -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
|
|
@@ -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"
|
|
@@ -342,6 +345,8 @@ void Win32MountingManager::forgetTag(Tag tag) {
|
|
|
342
345
|
scrollViews_.remove(tag);
|
|
343
346
|
textInputs_.remove(tag);
|
|
344
347
|
imageUris_.erase(tag);
|
|
348
|
+
imagePlaceholders_.erase(tag);
|
|
349
|
+
imageLoaded_.erase(tag);
|
|
345
350
|
switchValues_.erase(tag);
|
|
346
351
|
labels_.forget(tag);
|
|
347
352
|
liveRegions_.forget(tag);
|
|
@@ -872,6 +877,9 @@ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shado
|
|
|
872
877
|
const facebook::react::ImageSource source =
|
|
873
878
|
props->sources.empty() ? facebook::react::ImageSource{} : props->sources.front();
|
|
874
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);
|
|
875
883
|
|
|
876
884
|
// `tintColor`, applied before the load rather than in its callback: the tint
|
|
877
885
|
// is a prop and the image is a loader's answer, and either can arrive first.
|
|
@@ -914,6 +922,8 @@ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shado
|
|
|
914
922
|
return;
|
|
915
923
|
}
|
|
916
924
|
imageUris_[shadowView.tag] = uri;
|
|
925
|
+
imagePlaceholders_[shadowView.tag] = placeholder;
|
|
926
|
+
imageLoaded_.erase(shadowView.tag);
|
|
917
927
|
|
|
918
928
|
if (uri.empty()) {
|
|
919
929
|
view->setImage(nullptr, fit);
|
|
@@ -926,6 +936,38 @@ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shado
|
|
|
926
936
|
// the change did not take.
|
|
927
937
|
view->setImage(nullptr, fit);
|
|
928
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
|
+
|
|
929
971
|
// `<Image>`'s load events, which this host emitted none of until 2026-10-10:
|
|
930
972
|
// `onLoadStart`, `onProgress`, `onLoad`, `onError` and `onLoadEnd` all
|
|
931
973
|
// reached JavaScript on the other two hosts and nothing here ever dispatched
|
|
@@ -987,7 +1029,20 @@ void Win32MountingManager::applyImage(RnWin32View *view, const ShadowView &shado
|
|
|
987
1029
|
const bool stillWanted =
|
|
988
1030
|
target != nullptr && current != imageUris_.end() && current->second == uri;
|
|
989
1031
|
if (stillWanted) {
|
|
990
|
-
|
|
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
|
+
}
|
|
991
1046
|
applyAnimation(target, uri);
|
|
992
1047
|
// The screen is stale and no transaction is coming: the bytes arrived
|
|
993
1048
|
// on their own clock, after the mount that asked for them. On GTK and
|
|
@@ -1044,8 +1099,22 @@ win32::RnAccessibleFlag toFlag(bool value) {
|
|
|
1044
1099
|
std::string effectiveRole(const ShadowView &shadowView) {
|
|
1045
1100
|
if (const auto props =
|
|
1046
1101
|
std::dynamic_pointer_cast<const facebook::react::AccessibilityProps>(shadowView.props)) {
|
|
1047
|
-
|
|
1048
|
-
|
|
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;
|
|
1049
1118
|
}
|
|
1050
1119
|
}
|
|
1051
1120
|
if (shadowView.componentName != nullptr) {
|
|
@@ -1085,6 +1154,9 @@ void Win32MountingManager::applyAccessibility(RnWin32View *view, const ShadowVie
|
|
|
1085
1154
|
info.testId = props->testId;
|
|
1086
1155
|
// `accessibilityViewIsModal`, which UIA calls a dialog.
|
|
1087
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;
|
|
1088
1160
|
|
|
1089
1161
|
const auto &state = props->accessibilityState;
|
|
1090
1162
|
if (state.has_value()) {
|
|
@@ -1140,7 +1212,7 @@ void Win32MountingManager::applyAccessibility(RnWin32View *view, const ShadowVie
|
|
|
1140
1212
|
// that cannot be operated rather than stopping on one that does nothing.
|
|
1141
1213
|
const basalt::ControlState control = basalt::controlStateOf(shadowView);
|
|
1142
1214
|
const bool isControl = control.kind == basalt::ControlKind::Switch && !control.disabled;
|
|
1143
|
-
view->setFocusable((props
|
|
1215
|
+
view->setFocusable(basalt::keyboardFocusable(*props, isControl));
|
|
1144
1216
|
}
|
|
1145
1217
|
|
|
1146
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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#include "Win32TextInput.h"
|
|
2
2
|
|
|
3
3
|
#include "DirectWriteLayout.h"
|
|
4
|
+
#include "DragAndDrop.h"
|
|
4
5
|
#include "TextChecking.h"
|
|
5
6
|
#include "Win32PeerClip.h"
|
|
6
7
|
#include "Win32InputScopes.h"
|
|
@@ -134,8 +135,29 @@ void Win32TextInputManager::update(RnWin32View *view, const ShadowView &shadowVi
|
|
|
134
135
|
// that still behaves as one line. So a field that changes the prop has its
|
|
135
136
|
// peer rebuilt, which is rare enough to be worth the simplicity: React Native
|
|
136
137
|
// apps choose multiline per field rather than toggling it.
|
|
138
|
+
// `acceptDragAndDropTypes`: the kinds this field will take on a drop, which
|
|
139
|
+
// makes it a drop target in the sense core/DragAndDrop.h means. Android
|
|
140
|
+
// implements the prop as a filter over the platform's own drop handling and
|
|
141
|
+
// this is that filter: a list naming only text refuses a dragged file, so
|
|
142
|
+
// the cursor says no before the drop rather than the app rejecting it after.
|
|
143
|
+
//
|
|
144
|
+
// Cleared when the prop goes away, which is the case a field that stops
|
|
145
|
+
// accepting must not keep accepting.
|
|
146
|
+
if (view != nullptr) {
|
|
147
|
+
view->setDropAccepts(props != nullptr && props->acceptDragAndDropTypes.has_value()
|
|
148
|
+
? dropAcceptsFromTypes(*props->acceptDragAndDropTypes)
|
|
149
|
+
: DropAcceptsNone);
|
|
150
|
+
}
|
|
151
|
+
|
|
137
152
|
const bool multiline = props != nullptr && props->multiline;
|
|
138
|
-
|
|
153
|
+
// `scrollEnabled`, the other creation-time style bit. React Native documents
|
|
154
|
+
// it as multiline-only -- a single line scrolls sideways through
|
|
155
|
+
// `ES_AUTOHSCROLL` and the prop says nothing about that -- so it is only
|
|
156
|
+
// compared for a multiline field, and a single-line one that sets it does
|
|
157
|
+
// not get its peer rebuilt for nothing.
|
|
158
|
+
const bool scrollEnabled = props == nullptr || props->traits.scrollEnabled;
|
|
159
|
+
const bool scrollChanged = multiline && scrollEnabled != entry.scrollEnabled;
|
|
160
|
+
if (entry.control != nullptr && (multiline != entry.multiline || scrollChanged)) {
|
|
139
161
|
const bool hadFocus = GetFocus() == entry.control;
|
|
140
162
|
destroyPeer(entry);
|
|
141
163
|
entry.view = view;
|
|
@@ -161,9 +183,18 @@ void Win32TextInputManager::update(RnWin32View *view, const ShadowView &shadowVi
|
|
|
161
183
|
// scroll is what lets the caret leave the bottom of the box. No `WS_VSCROLL`
|
|
162
184
|
// on purpose -- a scrollbar inside a styled field is a Win32 scrollbar
|
|
163
185
|
// drawn over a React Native background, and neither other host shows one.
|
|
186
|
+
//
|
|
187
|
+
// `scrollEnabled: false` is that vertical scroll withheld, which is what
|
|
188
|
+
// the prop asks for: the field stops following the caret past the bottom
|
|
189
|
+
// of the box and the text below is simply not shown. An EDIT without
|
|
190
|
+
// `ES_AUTOVSCROLL` still accepts the text -- it is a display decision and
|
|
191
|
+
// not a limit, which is the same distinction `maxLength` is on the other
|
|
192
|
+
// side of.
|
|
164
193
|
entry.multiline = multiline;
|
|
194
|
+
entry.scrollEnabled = scrollEnabled;
|
|
165
195
|
const DWORD style = WS_CHILD | WS_CLIPSIBLINGS | ES_LEFT
|
|
166
|
-
| (multiline ? (ES_MULTILINE | ES_AUTOVSCROLL
|
|
196
|
+
| (multiline ? (ES_MULTILINE | (scrollEnabled ? ES_AUTOVSCROLL : 0u))
|
|
197
|
+
: ES_AUTOHSCROLL);
|
|
167
198
|
entry.control = CreateWindowEx(0,
|
|
168
199
|
L"EDIT",
|
|
169
200
|
L"",
|
|
@@ -264,6 +264,11 @@ class Win32TextInputManager {
|
|
|
264
264
|
// created and cannot be added afterwards, so this is what a change in the
|
|
265
265
|
// prop is compared against: a field that switches has its peer rebuilt.
|
|
266
266
|
bool multiline{false};
|
|
267
|
+
// `scrollEnabled`, which is part of the same creation-time decision as
|
|
268
|
+
// `multiline`: it is the `ES_AUTOVSCROLL` style bit, read when the window
|
|
269
|
+
// is made. Remembered so a change rebuilds the peer the way a change of
|
|
270
|
+
// `multiline` does.
|
|
271
|
+
bool scrollEnabled{true};
|
|
267
272
|
|
|
268
273
|
// Yoga resolves border and padding into contentInsets, so the same twelve
|
|
269
274
|
// points mean the same thing in a field as in a <View>. Applied by placing
|
|
@@ -21,17 +21,40 @@
|
|
|
21
21
|
// scale factor. GetDpiForWindow would answer for the window's monitor only.
|
|
22
22
|
#include <shellscalingapi.h>
|
|
23
23
|
|
|
24
|
+
#include "Win32AppWindow.h"
|
|
25
|
+
|
|
24
26
|
namespace basalt {
|
|
25
27
|
|
|
26
28
|
namespace {
|
|
27
29
|
|
|
30
|
+
// The window a call means, which is the lookup's answer where there is one.
|
|
31
|
+
HWND window(facebook::react::SurfaceId id) { return win32AppWindowFor(id); }
|
|
32
|
+
|
|
33
|
+
Win32AppWindowLookup &lookup() {
|
|
34
|
+
static Win32AppWindowLookup value;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
} // namespace
|
|
28
39
|
|
|
29
|
-
HWND
|
|
40
|
+
HWND win32AppWindow() {
|
|
30
41
|
HWND active = GetActiveWindow();
|
|
31
42
|
return active != nullptr ? active : GetForegroundWindow();
|
|
32
43
|
}
|
|
33
44
|
|
|
34
|
-
|
|
45
|
+
void setWin32AppWindowLookup(Win32AppWindowLookup value) { lookup() = std::move(value); }
|
|
46
|
+
|
|
47
|
+
HWND win32AppWindowFor(facebook::react::SurfaceId id) {
|
|
48
|
+
if (id != kAppWindow && lookup()) {
|
|
49
|
+
if (HWND found = lookup()(id)) {
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
// An id nobody knows is the app's own window rather than nothing: a window
|
|
53
|
+
// that closed between a render and the call after it is the ordinary way
|
|
54
|
+
// to get here. See AppKitAppWindow.h, which says the same.
|
|
55
|
+
}
|
|
56
|
+
return win32AppWindow();
|
|
57
|
+
}
|
|
35
58
|
|
|
36
59
|
// Every monitor, through the callback API that is the only way to ask.
|
|
37
60
|
//
|
|
@@ -99,9 +122,9 @@ PointerPosition pointerPosition() {
|
|
|
99
122
|
return where;
|
|
100
123
|
}
|
|
101
124
|
|
|
102
|
-
WindowBounds windowBounds() {
|
|
125
|
+
WindowBounds windowBounds(facebook::react::SurfaceId id) {
|
|
103
126
|
WindowBounds bounds;
|
|
104
|
-
HWND target = window();
|
|
127
|
+
HWND target = window(id);
|
|
105
128
|
if (target == nullptr) {
|
|
106
129
|
return bounds;
|
|
107
130
|
}
|
|
@@ -138,8 +161,8 @@ WindowBounds windowBounds() {
|
|
|
138
161
|
return bounds;
|
|
139
162
|
}
|
|
140
163
|
|
|
141
|
-
void setWindowSize(double width, double height) {
|
|
142
|
-
HWND target = window();
|
|
164
|
+
void setWindowSize(facebook::react::SurfaceId id, double width, double height) {
|
|
165
|
+
HWND target = window(id);
|
|
143
166
|
if (target == nullptr || width <= 0.0 || height <= 0.0) {
|
|
144
167
|
return;
|
|
145
168
|
}
|
|
@@ -160,8 +183,8 @@ void setWindowSize(double width, double height) {
|
|
|
160
183
|
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
161
184
|
}
|
|
162
185
|
|
|
163
|
-
void setWindowPosition(double x, double y) {
|
|
164
|
-
HWND target = window();
|
|
186
|
+
void setWindowPosition(facebook::react::SurfaceId id, double x, double y) {
|
|
187
|
+
HWND target = window(id);
|
|
165
188
|
if (target == nullptr) {
|
|
166
189
|
return;
|
|
167
190
|
}
|
|
@@ -174,8 +197,8 @@ void setWindowPosition(double x, double y) {
|
|
|
174
197
|
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
175
198
|
}
|
|
176
199
|
|
|
177
|
-
void centerWindow() {
|
|
178
|
-
HWND target = window();
|
|
200
|
+
void centerWindow(facebook::react::SurfaceId id) {
|
|
201
|
+
HWND target = window(id);
|
|
179
202
|
if (target == nullptr) {
|
|
180
203
|
return;
|
|
181
204
|
}
|
|
@@ -200,8 +223,8 @@ void centerWindow() {
|
|
|
200
223
|
SetWindowPos(target, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
201
224
|
}
|
|
202
225
|
|
|
203
|
-
void setWindowFullScreen(bool fullScreen) {
|
|
204
|
-
HWND target = window();
|
|
226
|
+
void setWindowFullScreen(facebook::react::SurfaceId id, bool fullScreen) {
|
|
227
|
+
HWND target = window(id);
|
|
205
228
|
if (target == nullptr) {
|
|
206
229
|
return;
|
|
207
230
|
}
|
|
@@ -252,7 +275,7 @@ void setWindowFullScreen(bool fullScreen) {
|
|
|
252
275
|
}
|
|
253
276
|
|
|
254
277
|
void applyWindowSizeLimits() {
|
|
255
|
-
HWND target =
|
|
278
|
+
HWND target = win32AppWindow();
|
|
256
279
|
if (target == nullptr) {
|
|
257
280
|
return;
|
|
258
281
|
}
|
|
@@ -275,7 +298,7 @@ void applyWindowSizeLimits() {
|
|
|
275
298
|
}
|
|
276
299
|
|
|
277
300
|
void setWindowResizable(bool resizable) {
|
|
278
|
-
HWND target =
|
|
301
|
+
HWND target = win32AppWindow();
|
|
279
302
|
if (target == nullptr) {
|
|
280
303
|
return;
|
|
281
304
|
}
|
|
@@ -303,7 +326,7 @@ void setWindowResizable(bool resizable) {
|
|
|
303
326
|
}
|
|
304
327
|
|
|
305
328
|
void setWindowAlwaysOnTop(bool alwaysOnTop) {
|
|
306
|
-
HWND target =
|
|
329
|
+
HWND target = win32AppWindow();
|
|
307
330
|
if (target == nullptr) {
|
|
308
331
|
return;
|
|
309
332
|
}
|
|
@@ -96,24 +96,19 @@ Win32WindowModule::Win32WindowModule(std::shared_ptr<facebook::react::CallInvoke
|
|
|
96
96
|
// The window's geometry, the same way the title bar's metrics go out: a
|
|
97
97
|
// device event, so `useWindow()` can hand an app live bounds rather than a
|
|
98
98
|
// snapshot it has to remember to refresh.
|
|
99
|
-
setWindowBoundsListener(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
object.setProperty(runtime, "fullScreen", bounds.fullScreen);
|
|
107
|
-
object.setProperty(runtime, "maximized", bounds.maximized);
|
|
108
|
-
args.emplace_back(Value(runtime, object));
|
|
109
|
-
});
|
|
110
|
-
});
|
|
99
|
+
setWindowBoundsListener(
|
|
100
|
+
[this](facebook::react::SurfaceId window, const WindowBounds &bounds) {
|
|
101
|
+
emitDeviceEvent(kWindowBoundsEvent,
|
|
102
|
+
[window, bounds](Runtime &runtime, std::vector<Value> &args) {
|
|
103
|
+
args.emplace_back(windowBoundsPayload(runtime, window, bounds));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
111
106
|
|
|
112
107
|
// And ask for the bounds once, now that something is listening. The host
|
|
113
108
|
// primes the cache when its window appears, but this module is built lazily
|
|
114
109
|
// -- the first time JavaScript asks for it -- so an app that mounts before
|
|
115
110
|
// then would read a window of no size and never be told otherwise.
|
|
116
|
-
postToUiThread([] { notifyWindowBoundsChanged(); });
|
|
111
|
+
postToUiThread([] { notifyWindowBoundsChanged(resolveWindow(kAppWindow)); });
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
Win32WindowModule::~Win32WindowModule() {
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
#endif
|
|
66
66
|
#include "Win32Packaging.h"
|
|
67
67
|
#include "PointerButtons.h"
|
|
68
|
+
#include "Win32AppWindow.h"
|
|
68
69
|
#include "WindowControl.h"
|
|
69
70
|
#include "SystemLocale.h"
|
|
70
71
|
#include "WindowHost.h"
|
|
@@ -1457,7 +1458,12 @@ LRESULT CALLBACK hostProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
|
|
|
1457
1458
|
// it needs its own case: WM_SIZE would never arrive, and `center()` would
|
|
1458
1459
|
// look like it had done nothing.
|
|
1459
1460
|
case WM_MOVE:
|
|
1460
|
-
|
|
1461
|
+
// Which window moved. `self` is null for the messages CreateWindowEx
|
|
1462
|
+
// sends before the window is in the list, and a move among those is not
|
|
1463
|
+
// one an app can see.
|
|
1464
|
+
if (self != nullptr) {
|
|
1465
|
+
basalt::notifyWindowBoundsChanged(self->surfaceId);
|
|
1466
|
+
}
|
|
1461
1467
|
return 0;
|
|
1462
1468
|
|
|
1463
1469
|
// `AppState`: whether this application is the one in front. Windows sends
|
|
@@ -1505,8 +1511,8 @@ LRESULT CALLBACK hostProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
|
|
|
1505
1511
|
}
|
|
1506
1512
|
// The window moved or was resized, which `useWindow()` hands an app as live
|
|
1507
1513
|
// bounds. Here rather than in a watcher of its own: this is already the one
|
|
1508
|
-
// place that learns it.
|
|
1509
|
-
basalt::notifyWindowBoundsChanged();
|
|
1514
|
+
// place that learns it, and it already knows which window it was.
|
|
1515
|
+
basalt::notifyWindowBoundsChanged(self->surfaceId);
|
|
1510
1516
|
// The inspector covers the main window, so it resizes with it -- and
|
|
1511
1517
|
// only with it: an error is about the app rather than about a window.
|
|
1512
1518
|
if (gHost.logBoxRoot != nullptr && self->surfaceId == kSurfaceId) {
|
|
@@ -2141,11 +2147,14 @@ facebook::react::SurfaceId openHostWindow(const NewWindowOptions &options) {
|
|
|
2141
2147
|
|
|
2142
2148
|
gHost.reactHost->startSurface(surfaceId,
|
|
2143
2149
|
options.component,
|
|
2144
|
-
options
|
|
2150
|
+
windowRootProps(options, surfaceId),
|
|
2145
2151
|
constraintsFor(width, height),
|
|
2146
2152
|
layoutContextFor(gHost.scaleFactor));
|
|
2147
2153
|
ShowWindow(made->window, SW_SHOW);
|
|
2148
2154
|
UpdateWindow(made->window);
|
|
2155
|
+
// Prime this window's bounds the way the main window's are primed at
|
|
2156
|
+
// startup; see the call beside that one.
|
|
2157
|
+
basalt::notifyWindowBoundsChanged(surfaceId);
|
|
2149
2158
|
std::fprintf(stderr,
|
|
2150
2159
|
"opened window %d for module %s\n",
|
|
2151
2160
|
static_cast<int>(surfaceId),
|
|
@@ -2369,10 +2378,18 @@ int main(int argc, char **argv) {
|
|
|
2369
2378
|
// being visible.
|
|
2370
2379
|
gHost.mountingManager->setOnDidMount(syncPeersAndRepaint);
|
|
2371
2380
|
|
|
2381
|
+
// How the window module finds a window by id, and which one a call that
|
|
2382
|
+
// names none means. See Win32AppWindow.h.
|
|
2383
|
+
basalt::setWin32AppWindowLookup([](facebook::react::SurfaceId surfaceId) -> HWND {
|
|
2384
|
+
HostWindow *found = gHost.windowFor(surfaceId);
|
|
2385
|
+
return found == nullptr ? nullptr : found->window;
|
|
2386
|
+
});
|
|
2387
|
+
basalt::setAppWindowId(kSurfaceId);
|
|
2388
|
+
|
|
2372
2389
|
// Prime the bounds cache, which `getBounds()` answers from: without this an
|
|
2373
2390
|
// app's first render sees a window of no size, and only a later resize
|
|
2374
2391
|
// corrects it. See core/WindowBoundsCache.cpp.
|
|
2375
|
-
basalt::notifyWindowBoundsChanged();
|
|
2392
|
+
basalt::notifyWindowBoundsChanged(kSurfaceId);
|
|
2376
2393
|
|
|
2377
2394
|
// And the display list, for the same reason and off the same seam: an app
|
|
2378
2395
|
// asking which screens exist before anything has been plugged or unplugged
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "basalt-win32",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "The Windows view layer for basalt-core: a Win32 and Direct2D view layer, a Fabric mounting manager, and a host. Everything behind them is in basalt-core.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
"react-native": "*"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"basalt-core": "0.2.
|
|
25
|
+
"basalt-core": "0.2.3"
|
|
26
26
|
}
|
|
27
27
|
}
|