react-native-windows 0.84.0-preview.7 → 0.84.0
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/Common/unicode.cpp +36 -0
- package/Common/unicode.h +8 -0
- package/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +5 -0
- package/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +106 -24
- package/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +11 -0
- package/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp +4 -0
- package/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h +1 -0
- package/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +6 -4
- package/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +28 -6
- package/Microsoft.ReactNative/Fabric/Composition/TextDrawing.cpp +1 -1
- package/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp +80 -0
- package/Microsoft.ReactNative/Fabric/Composition/TooltipService.h +17 -0
- package/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp +1 -1
- package/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +3 -2
- package/Microsoft.ReactNative/Utils/LocalBundleReader.cpp +33 -25
- package/Microsoft.ReactNative/Utils/LocalBundleReader.h +5 -3
- package/Microsoft.ReactNative/Utils/UwpScriptStore.cpp +2 -2
- package/Microsoft.ReactNative/Utils/UwpScriptStore.h +3 -2
- package/Microsoft.ReactNative.Managed/Microsoft.ReactNative.Managed.csproj +3 -3
- package/Microsoft.ReactNative.Managed.CodeGen/Microsoft.ReactNative.Managed.CodeGen.csproj +10 -10
- package/Microsoft.ReactNative.Managed.CodeGen/Properties/PublishProfiles/DeployAsTool-Debug.pubxml +1 -1
- package/Microsoft.ReactNative.Managed.CodeGen/Properties/PublishProfiles/DeployAsTool-Release.pubxml +1 -1
- package/PropertySheets/Generated/PackageVersion.g.props +2 -2
- package/PropertySheets/JSEngine.props +1 -1
- package/PropertySheets/React.Cpp.props +4 -3
- package/PropertySheets/WinUI.props +2 -2
- package/Scripts/JustMyXaml.ps1 +8 -8
- package/Scripts/UnitTest.ps1 +3 -1
- package/Scripts/rnw-dependencies.ps1 +15 -16
- package/Shared/DevSupportManager.cpp +55 -48
- package/Shared/DevSupportManager.h +0 -1
- package/Shared/Modules/IWebSocketModuleContentHandler.h +15 -0
- package/Shared/Modules/WebSocketModule.cpp +8 -3
- package/Shared/Networking/DefaultBlobResource.cpp +37 -0
- package/Shared/Networking/DefaultBlobResource.h +12 -0
- package/Shared/Networking/WinRTWebSocketResource.h +5 -1
- package/just-task.js +13 -2
- package/package.json +6 -6
- package/templates/cpp-app/windows/MyApp/MyApp.vcxproj +1 -1
- package/templates/cpp-lib/windows/MyLib/MyLib.vcxproj +1 -1
package/Common/unicode.cpp
CHANGED
|
@@ -93,6 +93,42 @@ std::wstring Utf8ToUtf16(const std::string &utf8) {
|
|
|
93
93
|
return Utf8ToUtf16(utf8.c_str(), utf8.length());
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
size_t Utf8ToUtf16Length(const char *utf8, size_t utf8Len) {
|
|
97
|
+
if (utf8Len == 0) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (utf8Len > static_cast<size_t>((std::numeric_limits<int>::max)())) {
|
|
102
|
+
throw std::overflow_error("Length of input string to Utf8ToUtf16Length() must fit into an int.");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const int utf8Length = static_cast<int>(utf8Len);
|
|
106
|
+
|
|
107
|
+
constexpr DWORD flags = 0;
|
|
108
|
+
|
|
109
|
+
const int utf16Length = ::MultiByteToWideChar(
|
|
110
|
+
CP_UTF8, // Source string is in UTF-8.
|
|
111
|
+
flags, // Conversion flags.
|
|
112
|
+
utf8, // Source UTF-8 string pointer.
|
|
113
|
+
utf8Length, // Length of the source UTF-8 string, in chars.
|
|
114
|
+
nullptr, // Do not convert, just request the size.
|
|
115
|
+
0 // Request size of destination buffer, in wchar_ts.
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (utf16Length == 0) {
|
|
119
|
+
throw UnicodeConversionException(
|
|
120
|
+
"Cannot get result string length when converting from UTF-8 to UTF-16 "
|
|
121
|
+
"(MultiByteToWideChar failed).",
|
|
122
|
+
GetLastError());
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return static_cast<size_t>(utf16Length);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
size_t Utf8ToUtf16Length(const std::string &utf8) {
|
|
129
|
+
return Utf8ToUtf16Length(utf8.c_str(), utf8.length());
|
|
130
|
+
}
|
|
131
|
+
|
|
96
132
|
#if _HAS_CXX17
|
|
97
133
|
std::wstring Utf8ToUtf16(const std::string_view &utf8) {
|
|
98
134
|
return Utf8ToUtf16(utf8.data(), utf8.length());
|
package/Common/unicode.h
CHANGED
|
@@ -55,6 +55,14 @@ class UnicodeConversionException : public std::runtime_error {
|
|
|
55
55
|
/* (4) */ std::wstring Utf8ToUtf16(const std::string_view &utf8);
|
|
56
56
|
#endif
|
|
57
57
|
|
|
58
|
+
// The following functions return the length of the UTF-16 string that would
|
|
59
|
+
// result from converting the input UTF-8 string, without actually performing
|
|
60
|
+
// the conversion or allocating a temporary std::wstring. This is useful in
|
|
61
|
+
// hot paths where only the length is needed (e.g. DirectWrite text ranges).
|
|
62
|
+
//
|
|
63
|
+
size_t Utf8ToUtf16Length(const char *utf8, size_t utf8Len);
|
|
64
|
+
size_t Utf8ToUtf16Length(const std::string &utf8);
|
|
65
|
+
|
|
58
66
|
// The following functions convert UTF-16BE strings to UTF-8 strings. Their
|
|
59
67
|
// behaviors mirror those of the above Utf8ToUtf16 functions.
|
|
60
68
|
//
|
|
@@ -1064,6 +1064,11 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::get_SelectionContainer(I
|
|
|
1064
1064
|
*pRetVal = nullptr;
|
|
1065
1065
|
|
|
1066
1066
|
auto selectionContainerView = GetSelectionContainer();
|
|
1067
|
+
// Per UIA spec, returning S_OK with *pRetVal == nullptr is correct when the element
|
|
1068
|
+
// is not contained within a selection container.
|
|
1069
|
+
if (!selectionContainerView)
|
|
1070
|
+
return S_OK;
|
|
1071
|
+
|
|
1067
1072
|
auto uiaProvider =
|
|
1068
1073
|
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(selectionContainerView)
|
|
1069
1074
|
->EnsureUiaProvider();
|
|
@@ -224,6 +224,28 @@ void CompositionEventHandler::Initialize() noexcept {
|
|
|
224
224
|
}
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
+
// Issue #16047: when ScrollView calls VisualInteractionSource::TryRedirectForManipulation
|
|
228
|
+
// and the OS hands the pointer over to the InteractionTracker, WinAppSDK
|
|
229
|
+
// does not fire PointerCaptureLost on this source — but it does fire
|
|
230
|
+
// PointerRoutedAway. Treat it the same way as captureloss: cancel any
|
|
231
|
+
// active touch RN is tracking for this pointer so Pressables don't get
|
|
232
|
+
// stuck in their pressed state.
|
|
233
|
+
m_pointerRoutedAwayToken =
|
|
234
|
+
pointerSource.PointerRoutedAway([wkThis = weak_from_this()](
|
|
235
|
+
winrt::Microsoft::UI::Input::InputPointerSource const &,
|
|
236
|
+
winrt::Microsoft::UI::Input::PointerEventArgs const &args) {
|
|
237
|
+
if (auto strongThis = wkThis.lock()) {
|
|
238
|
+
if (auto strongRootView = strongThis->m_wkRootView.get()) {
|
|
239
|
+
if (strongThis->SurfaceId() == -1)
|
|
240
|
+
return;
|
|
241
|
+
|
|
242
|
+
auto pp = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
|
|
243
|
+
args.CurrentPoint(), strongRootView.ScaleFactor());
|
|
244
|
+
strongThis->onPointerRoutedAway(pp, args.KeyModifiers());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
227
249
|
m_pointerWheelChangedToken =
|
|
228
250
|
pointerSource.PointerWheelChanged([wkThis = weak_from_this()](
|
|
229
251
|
winrt::Microsoft::UI::Input::InputPointerSource const &,
|
|
@@ -369,6 +391,7 @@ CompositionEventHandler::~CompositionEventHandler() {
|
|
|
369
391
|
pointerSource.PointerReleased(m_pointerReleasedToken);
|
|
370
392
|
pointerSource.PointerMoved(m_pointerMovedToken);
|
|
371
393
|
pointerSource.PointerCaptureLost(m_pointerCaptureLostToken);
|
|
394
|
+
pointerSource.PointerRoutedAway(m_pointerRoutedAwayToken);
|
|
372
395
|
pointerSource.PointerWheelChanged(m_pointerWheelChangedToken);
|
|
373
396
|
pointerSource.PointerExited(m_pointerExitedToken);
|
|
374
397
|
auto keyboardSource = winrt::Microsoft::UI::Input::InputKeyboardSource::GetForIsland(island);
|
|
@@ -1092,16 +1115,74 @@ void CompositionEventHandler::onPointerCaptureLost(
|
|
|
1092
1115
|
if (SurfaceId() == -1)
|
|
1093
1116
|
return;
|
|
1094
1117
|
|
|
1095
|
-
if (m_pointerCapturingComponentTag) {
|
|
1118
|
+
if (m_pointerCapturingComponentTag != -1) {
|
|
1096
1119
|
// copy array to avoid iterator being invalidated during deletion
|
|
1097
1120
|
std::unordered_set<PointerId> capturedPointers = m_capturedPointers;
|
|
1098
1121
|
|
|
1099
1122
|
for (auto pointerId : capturedPointers) {
|
|
1100
1123
|
releasePointerCapture(pointerId, m_pointerCapturingComponentTag);
|
|
1124
|
+
|
|
1125
|
+
// Cancel any active touch for this pointer so React Native is notified that
|
|
1126
|
+
// the touch ended. Without this, m_activeTouches retains a zombie entry and
|
|
1127
|
+
// RN JS is never told the touch is gone — leaving Pressables stuck in a
|
|
1128
|
+
// pressed state after a system-interrupted gesture (e.g. system back swipe,
|
|
1129
|
+
// Alt+Tab, another window coming foreground).
|
|
1130
|
+
auto activeTouch = m_activeTouches.find(pointerId);
|
|
1131
|
+
if (activeTouch != m_activeTouches.end()) {
|
|
1132
|
+
ActiveTouch cancelledTouchCopy = std::move(activeTouch->second);
|
|
1133
|
+
m_activeTouches.erase(activeTouch);
|
|
1134
|
+
if (cancelledTouchCopy.eventEmitter) {
|
|
1135
|
+
DispatchSynthesizedTouchCancelForActiveTouch(cancelledTouchCopy, pointerPoint, keyModifiers);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1101
1138
|
}
|
|
1102
1139
|
|
|
1103
1140
|
m_pointerCapturingComponentTag = -1;
|
|
1104
1141
|
}
|
|
1142
|
+
|
|
1143
|
+
// Defense-in-depth cleanup for the specific pointer that lost capture, even
|
|
1144
|
+
// when no JS-level CapturePointer was ever issued. The ScrollView
|
|
1145
|
+
// TryRedirectForManipulation path comes in via PointerRoutedAway, not
|
|
1146
|
+
// PointerCaptureLost (see onPointerRoutedAway and issue #16047), so this
|
|
1147
|
+
// path covers the remaining system-driven losses (focus change, another
|
|
1148
|
+
// window stealing input, system back gesture, etc.).
|
|
1149
|
+
CancelActiveTouchForPointerInternal(pointerPoint.PointerId(), pointerPoint, keyModifiers);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
void CompositionEventHandler::onPointerRoutedAway(
|
|
1153
|
+
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
|
|
1154
|
+
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
|
|
1155
|
+
if (SurfaceId() == -1)
|
|
1156
|
+
return;
|
|
1157
|
+
|
|
1158
|
+
// Issue #16047: WinAppSDK fires PointerRoutedAway when the OS hands the
|
|
1159
|
+
// pointer to another InputPointerSource — most importantly for us, when
|
|
1160
|
+
// ScrollView calls VisualInteractionSource::TryRedirectForManipulation and
|
|
1161
|
+
// the InteractionTracker takes the gesture for scrolling. We never get
|
|
1162
|
+
// PointerMoved / PointerReleased / PointerCaptureLost for that pointer
|
|
1163
|
+
// afterwards, so without this cleanup m_activeTouches keeps a zombie entry
|
|
1164
|
+
// and the originally-pressed Pressable stays stuck in its pressed state.
|
|
1165
|
+
CancelActiveTouchForPointerInternal(pointerPoint.PointerId(), pointerPoint, keyModifiers);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
bool CompositionEventHandler::CancelActiveTouchForPointerInternal(
|
|
1169
|
+
PointerId pointerId,
|
|
1170
|
+
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
|
|
1171
|
+
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept {
|
|
1172
|
+
auto activeTouch = m_activeTouches.find(pointerId);
|
|
1173
|
+
if (activeTouch == m_activeTouches.end()) {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
ActiveTouch cancelledTouchCopy = std::move(activeTouch->second);
|
|
1178
|
+
m_activeTouches.erase(activeTouch);
|
|
1179
|
+
|
|
1180
|
+
if (!cancelledTouchCopy.eventEmitter) {
|
|
1181
|
+
return false;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
DispatchSynthesizedTouchCancelForActiveTouch(cancelledTouchCopy, pointerPoint, keyModifiers);
|
|
1185
|
+
return true;
|
|
1105
1186
|
}
|
|
1106
1187
|
|
|
1107
1188
|
void CompositionEventHandler::onPointerMoved(
|
|
@@ -1352,7 +1433,7 @@ void CompositionEventHandler::onPointerPressed(
|
|
|
1352
1433
|
|
|
1353
1434
|
UpdateActiveTouch(activeTouch, ptScaled, ptLocal);
|
|
1354
1435
|
|
|
1355
|
-
activeTouch.isPrimary =
|
|
1436
|
+
activeTouch.isPrimary = pointerPoint.Properties().IsPrimary();
|
|
1356
1437
|
// Map the Windows pointer ID to a small identifier (0–19) safe for use as a JS array index.
|
|
1357
1438
|
// Windows touch IDs can be arbitrarily large (e.g. 2233), which causes React Native to warn
|
|
1358
1439
|
// and corrupts touch state, leaving Pressables stuck after a scroll.
|
|
@@ -1600,16 +1681,6 @@ bool CompositionEventHandler::IsPointerWithinInitialTree(const ActiveTouch &acti
|
|
|
1600
1681
|
currentView = currentView.Parent();
|
|
1601
1682
|
}
|
|
1602
1683
|
|
|
1603
|
-
// Fallback: if the pointer drifted spatially but the original target
|
|
1604
|
-
// is still structurally within the initial tree, honor the tap.
|
|
1605
|
-
// This provides touch-device tolerance for finger drift.
|
|
1606
|
-
auto targetView = viewRegistry.componentViewDescriptorWithTag(activeTouch.touch.target).view;
|
|
1607
|
-
while (targetView) {
|
|
1608
|
-
if (targetView.Tag() == initialTag)
|
|
1609
|
-
return true;
|
|
1610
|
-
targetView = targetView.Parent();
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
1684
|
return false;
|
|
1614
1685
|
}
|
|
1615
1686
|
|
|
@@ -1671,7 +1742,15 @@ void CompositionEventHandler::DispatchTouchEvent(
|
|
|
1671
1742
|
|
|
1672
1743
|
facebook::react::TouchEvent event;
|
|
1673
1744
|
|
|
1674
|
-
|
|
1745
|
+
// First pass: build changedTouches and the set of unique emitters from every active
|
|
1746
|
+
// touch. The per-pointer PointerEvent dispatch (onPointerDown/Move/Up/Cancel/Click) is
|
|
1747
|
+
// fired only for the touch whose state actually changed — non-changed touches contribute
|
|
1748
|
+
// to the W3C TouchEvent's touches/targetTouches sets in the loops below but must not
|
|
1749
|
+
// re-fire pointer events of their own. Previously we dispatched the per-pointer event
|
|
1750
|
+
// for every entry in m_activeTouches, which produced duplicated onPointerMove on
|
|
1751
|
+
// non-moving fingers and replayed onPointerUp/onClick on stale targets after the OS
|
|
1752
|
+
// reclaimed a pointer (e.g. ScrollView manipulation redirect leaving a zombie touch).
|
|
1753
|
+
const ActiveTouch *changedTouch = nullptr;
|
|
1675
1754
|
for (const auto &pair : m_activeTouches) {
|
|
1676
1755
|
const auto &activeTouch = pair.second;
|
|
1677
1756
|
|
|
@@ -1680,14 +1759,17 @@ void CompositionEventHandler::DispatchTouchEvent(
|
|
|
1680
1759
|
}
|
|
1681
1760
|
|
|
1682
1761
|
if (pair.first == pointerId) {
|
|
1762
|
+
changedTouch = &activeTouch;
|
|
1683
1763
|
event.changedTouches.insert(activeTouch.touch);
|
|
1684
1764
|
}
|
|
1685
1765
|
uniqueEventEmitters.insert(activeTouch.eventEmitter);
|
|
1766
|
+
}
|
|
1686
1767
|
|
|
1687
|
-
|
|
1768
|
+
if (changedTouch) {
|
|
1769
|
+
facebook::react::PointerEvent pointerEvent = CreatePointerEventFromActiveTouch(*changedTouch, eventType);
|
|
1688
1770
|
|
|
1689
1771
|
winrt::Microsoft::ReactNative::ComponentView targetView{nullptr};
|
|
1690
|
-
bool shouldLeave = (eventType == TouchEventType::End &&
|
|
1772
|
+
bool shouldLeave = (eventType == TouchEventType::End && changedTouch->shouldLeaveWhenReleased) ||
|
|
1691
1773
|
eventType == TouchEventType::Cancel;
|
|
1692
1774
|
if (!shouldLeave) {
|
|
1693
1775
|
auto *rootViewForHit = RootComponentView();
|
|
@@ -1702,29 +1784,29 @@ void CompositionEventHandler::DispatchTouchEvent(
|
|
|
1702
1784
|
}
|
|
1703
1785
|
}
|
|
1704
1786
|
|
|
1705
|
-
auto handler = [this,
|
|
1787
|
+
auto handler = [this, changedTouch, eventType, &pointerEvent](
|
|
1706
1788
|
std::vector<winrt::Microsoft::ReactNative::ComponentView> &eventPathViews) {
|
|
1707
1789
|
switch (eventType) {
|
|
1708
1790
|
case TouchEventType::Start:
|
|
1709
|
-
|
|
1791
|
+
changedTouch->eventEmitter->onPointerDown(pointerEvent);
|
|
1710
1792
|
break;
|
|
1711
1793
|
case TouchEventType::Move: {
|
|
1712
|
-
|
|
1794
|
+
changedTouch->eventEmitter->onPointerMove(pointerEvent);
|
|
1713
1795
|
break;
|
|
1714
1796
|
}
|
|
1715
1797
|
case TouchEventType::End:
|
|
1716
|
-
|
|
1798
|
+
changedTouch->eventEmitter->onPointerUp(pointerEvent);
|
|
1717
1799
|
if (pointerEvent.isPrimary && pointerEvent.button == 0) {
|
|
1718
|
-
if (IsPointerWithinInitialTree(
|
|
1719
|
-
|
|
1800
|
+
if (IsPointerWithinInitialTree(*changedTouch)) {
|
|
1801
|
+
changedTouch->eventEmitter->onClick(pointerEvent);
|
|
1720
1802
|
}
|
|
1721
|
-
} /* else if (IsPointerWithinInitialTree(
|
|
1722
|
-
|
|
1803
|
+
} /* else if (IsPointerWithinInitialTree(*changedTouch)) {
|
|
1804
|
+
changedTouch->eventEmitter->onAuxClick(pointerEvent);
|
|
1723
1805
|
} */
|
|
1724
1806
|
break;
|
|
1725
1807
|
case TouchEventType::Cancel:
|
|
1726
1808
|
case TouchEventType::CaptureLost:
|
|
1727
|
-
|
|
1809
|
+
changedTouch->eventEmitter->onPointerCancel(pointerEvent);
|
|
1728
1810
|
break;
|
|
1729
1811
|
}
|
|
1730
1812
|
};
|
|
@@ -66,6 +66,9 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
|
|
|
66
66
|
void onPointerCaptureLost(
|
|
67
67
|
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
|
|
68
68
|
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept;
|
|
69
|
+
void onPointerRoutedAway(
|
|
70
|
+
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
|
|
71
|
+
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept;
|
|
69
72
|
void onKeyDown(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept;
|
|
70
73
|
void onKeyUp(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept;
|
|
71
74
|
void onCharacterReceived(
|
|
@@ -156,6 +159,13 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
|
|
|
156
159
|
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
|
|
157
160
|
winrt::Windows::System::VirtualKeyModifiers keyModifiers);
|
|
158
161
|
|
|
162
|
+
// Look up the active touch for pointerId, erase it, and dispatch cancel events.
|
|
163
|
+
// Returns true iff a touch was found and cancel events were dispatched.
|
|
164
|
+
bool CancelActiveTouchForPointerInternal(
|
|
165
|
+
PointerId pointerId,
|
|
166
|
+
const winrt::Microsoft::ReactNative::Composition::Input::PointerPoint &pointerPoint,
|
|
167
|
+
winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept;
|
|
168
|
+
|
|
159
169
|
std::vector<winrt::Microsoft::ReactNative::ComponentView> GetTouchableViewsInPathToRoot(
|
|
160
170
|
const winrt::Microsoft::ReactNative::ComponentView &componentView);
|
|
161
171
|
|
|
@@ -186,6 +196,7 @@ class CompositionEventHandler : public std::enable_shared_from_this<CompositionE
|
|
|
186
196
|
winrt::event_token m_pointerMovedToken;
|
|
187
197
|
winrt::event_token m_pointerWheelChangedToken;
|
|
188
198
|
winrt::event_token m_pointerCaptureLostToken;
|
|
199
|
+
winrt::event_token m_pointerRoutedAwayToken;
|
|
189
200
|
winrt::event_token m_pointerExitedToken;
|
|
190
201
|
winrt::event_token m_keyDownToken;
|
|
191
202
|
winrt::event_token m_keyUpToken;
|
|
@@ -130,6 +130,7 @@ void ImageComponentView::didReceiveImage(const std::shared_ptr<ImageResponseImag
|
|
|
130
130
|
#endif
|
|
131
131
|
|
|
132
132
|
m_imageResponseImage = imageResponseImage;
|
|
133
|
+
m_requiresImageRedraw = true;
|
|
133
134
|
ensureDrawingSurface();
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -310,6 +311,9 @@ void ImageComponentView::ensureDrawingSurface() noexcept {
|
|
|
310
311
|
} else if (m_imageResponseImage->m_brushFactory) {
|
|
311
312
|
Visual().as<Experimental::ISpriteVisual>().Brush(
|
|
312
313
|
m_imageResponseImage->m_brushFactory(m_reactContext.Handle(), m_compContext));
|
|
314
|
+
} else if (m_requiresImageRedraw) {
|
|
315
|
+
m_requiresImageRedraw = false;
|
|
316
|
+
DrawImage();
|
|
313
317
|
}
|
|
314
318
|
}
|
|
315
319
|
|
|
@@ -96,6 +96,7 @@ struct ImageComponentView : ImageComponentViewT<ImageComponentView, ViewComponen
|
|
|
96
96
|
winrt::Microsoft::ReactNative::Composition::Experimental::IDrawingSurfaceBrush m_drawingSurface;
|
|
97
97
|
std::shared_ptr<ImageResponseImage> m_imageResponseImage;
|
|
98
98
|
std::shared_ptr<WindowsImageResponseObserver> m_imageResponseObserver;
|
|
99
|
+
bool m_requiresImageRedraw{true};
|
|
99
100
|
facebook::react::ImageShadowNode::ConcreteState::Shared m_state;
|
|
100
101
|
};
|
|
101
102
|
|
|
@@ -153,11 +153,12 @@ facebook::react::SharedViewEventEmitter ParagraphComponentView::eventEmitterAtPo
|
|
|
153
153
|
uint32_t textPosition = metrics.textPosition;
|
|
154
154
|
|
|
155
155
|
for (auto fragment : m_attributedStringBox.getValue().getFragments()) {
|
|
156
|
-
|
|
156
|
+
uint32_t utf16Length = static_cast<uint32_t>(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string));
|
|
157
|
+
if (textPosition < utf16Length) {
|
|
157
158
|
return std::static_pointer_cast<const facebook::react::ViewEventEmitter>(
|
|
158
159
|
fragment.parentShadowView.eventEmitter);
|
|
159
160
|
}
|
|
160
|
-
textPosition -=
|
|
161
|
+
textPosition -= utf16Length;
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
}
|
|
@@ -210,10 +211,11 @@ bool ParagraphComponentView::IsTextSelectableAtPoint(facebook::react::Point pt)
|
|
|
210
211
|
|
|
211
212
|
// Finds which fragment contains this text position
|
|
212
213
|
for (auto fragment : m_attributedStringBox.getValue().getFragments()) {
|
|
213
|
-
|
|
214
|
+
uint32_t utf16Length = static_cast<uint32_t>(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string));
|
|
215
|
+
if (textPosition < utf16Length) {
|
|
214
216
|
return true;
|
|
215
217
|
}
|
|
216
|
-
textPosition -=
|
|
218
|
+
textPosition -= utf16Length;
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
#include <functional>
|
|
24
24
|
#include "ContentIslandComponentView.h"
|
|
25
25
|
#include "JSValueReader.h"
|
|
26
|
+
#include "ReactNativeIsland.h"
|
|
26
27
|
#include "RootComponentView.h"
|
|
27
28
|
|
|
28
29
|
namespace winrt::Microsoft::ReactNative::Composition::implementation {
|
|
@@ -849,13 +850,27 @@ void ScrollViewComponentView::updateStateWithContentOffset() noexcept {
|
|
|
849
850
|
return;
|
|
850
851
|
}
|
|
851
852
|
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
853
|
+
// Issue #16047: m_scrollVisual.ScrollPosition() returns the InteractionTracker
|
|
854
|
+
// position in PHYSICAL pixels (the visual is sized as
|
|
855
|
+
// layoutMetrics.frame.size.* * pointScaleFactor — see updateLayoutMetrics /
|
|
856
|
+
// updateContentVisualSize) but ScrollViewShadowNode state's contentOffset is
|
|
857
|
+
// in DIPs. Without the conversion, JS UIManager.measure() over-subtracts by
|
|
858
|
+
// pointScaleFactor on non-100% display scales, leaving Pressables inside a
|
|
859
|
+
// scrolled ScrollView with stale page-space bounds that don't contain the
|
|
860
|
+
// touch — Pressability fires LEAVE_PRESS_RECT inside pressIn and suppresses
|
|
861
|
+
// press. The JS-event-emitter paths in this file (see lines using
|
|
862
|
+
// args.Position() / pointScaleFactor) already do this division.
|
|
863
|
+
auto rawScrollPosition = m_scrollVisual.ScrollPosition();
|
|
864
|
+
const float pointScaleFactor = m_layoutMetrics.pointScaleFactor > 0.0f ? m_layoutMetrics.pointScaleFactor : 1.0f;
|
|
865
|
+
facebook::react::Point contentOffsetDips{
|
|
866
|
+
rawScrollPosition.x / pointScaleFactor, rawScrollPosition.y / pointScaleFactor};
|
|
867
|
+
|
|
868
|
+
m_verticalScrollbarComponent->ContentOffset(rawScrollPosition);
|
|
869
|
+
m_horizontalScrollbarComponent->ContentOffset(rawScrollPosition);
|
|
870
|
+
|
|
871
|
+
m_state->updateState([contentOffsetDips](const facebook::react::ScrollViewShadowNode::ConcreteState::Data &data) {
|
|
857
872
|
auto newData = data;
|
|
858
|
-
newData.contentOffset =
|
|
873
|
+
newData.contentOffset = contentOffsetDips;
|
|
859
874
|
return std::make_shared<facebook::react::ScrollViewShadowNode::ConcreteState::Data const>(newData);
|
|
860
875
|
});
|
|
861
876
|
}
|
|
@@ -1389,6 +1404,13 @@ winrt::Microsoft::ReactNative::Composition::Experimental::IVisual ScrollViewComp
|
|
|
1389
1404
|
[this](
|
|
1390
1405
|
winrt::IInspectable const & /*sender*/,
|
|
1391
1406
|
winrt::Microsoft::ReactNative::Composition::Experimental::IScrollPositionChangedArgs const &args) {
|
|
1407
|
+
// Issue #16047: push the FINAL settled scroll position into Fabric's
|
|
1408
|
+
// shadow tree before notifying JS. The per-frame ScrollPositionChanged
|
|
1409
|
+
// updates can drop the last inertia delta, leaving contentOffset stale
|
|
1410
|
+
// and JS UIManager.measure() returning pre-settle-relative bounds.
|
|
1411
|
+
// ScrollEndDrag / ScrollBeginDrag already call this; momentum-end was
|
|
1412
|
+
// the missing completion path.
|
|
1413
|
+
updateStateWithContentOffset();
|
|
1392
1414
|
auto eventEmitter = GetEventEmitter();
|
|
1393
1415
|
if (eventEmitter) {
|
|
1394
1416
|
auto scrollMetrics = getScrollMetrics(eventEmitter, args);
|
|
@@ -66,7 +66,7 @@ void RenderText(
|
|
|
66
66
|
unsigned int position = 0;
|
|
67
67
|
unsigned int length = 0;
|
|
68
68
|
for (auto fragment : attributedString.getFragments()) {
|
|
69
|
-
length = static_cast<UINT32>(fragment.string
|
|
69
|
+
length = static_cast<UINT32>(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string));
|
|
70
70
|
DWRITE_TEXT_RANGE range = {position, length};
|
|
71
71
|
if (fragment.textAttributes.foregroundColor &&
|
|
72
72
|
(fragment.textAttributes.foregroundColor != textAttributes.foregroundColor) ||
|
|
@@ -169,12 +169,22 @@ TooltipTracker::TooltipTracker(
|
|
|
169
169
|
view.PointerEntered({this, &TooltipTracker::OnPointerEntered});
|
|
170
170
|
view.PointerExited({this, &TooltipTracker::OnPointerExited});
|
|
171
171
|
view.PointerMoved({this, &TooltipTracker::OnPointerMoved});
|
|
172
|
+
view.GotFocus({this, &TooltipTracker::OnGotFocus});
|
|
173
|
+
view.LostFocus({this, &TooltipTracker::OnLostFocus});
|
|
172
174
|
view.Unmounted({this, &TooltipTracker::OnUnmounted});
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
TooltipTracker::~TooltipTracker() {
|
|
176
178
|
DestroyTimer();
|
|
177
179
|
DestroyTooltip();
|
|
180
|
+
m_outer->NotifyDismiss(this);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
void TooltipTracker::DismissForExternalRequest() noexcept {
|
|
184
|
+
// Service is already updating its active slot; do not call back into it.
|
|
185
|
+
m_focusTooltip = false;
|
|
186
|
+
DestroyTimer();
|
|
187
|
+
DestroyTooltip();
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
facebook::react::Tag TooltipTracker::Tag() const noexcept {
|
|
@@ -192,6 +202,9 @@ void TooltipTracker::OnPointerEntered(
|
|
|
192
202
|
auto pp = args.GetCurrentPoint(-1);
|
|
193
203
|
m_pos = pp.Position();
|
|
194
204
|
|
|
205
|
+
// Claim the single tooltip slot, dismissing any other tracker's pending or visible tooltip.
|
|
206
|
+
m_outer->NotifyShow(this);
|
|
207
|
+
|
|
195
208
|
m_timer = winrt::Microsoft::ReactNative::Timer::Create(m_properties.Handle());
|
|
196
209
|
m_timer.Interval(std::chrono::milliseconds(toolTipTimeToShowMs));
|
|
197
210
|
m_timer.Tick({this, &TooltipTracker::OnTick});
|
|
@@ -225,6 +238,56 @@ void TooltipTracker::OnPointerExited(
|
|
|
225
238
|
return;
|
|
226
239
|
DestroyTimer();
|
|
227
240
|
DestroyTooltip();
|
|
241
|
+
m_outer->NotifyDismiss(this);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
void TooltipTracker::OnGotFocus(
|
|
245
|
+
const winrt::Windows::Foundation::IInspectable & /*sender*/,
|
|
246
|
+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs & /*args*/) noexcept {
|
|
247
|
+
// Skip if a mouse-driven tooltip or its dwell timer is already in flight on this view.
|
|
248
|
+
if (m_hwndTip || m_timer) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
auto view = m_view.view();
|
|
253
|
+
if (!view) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
auto viewCompView = view.try_as<winrt::Microsoft::ReactNative::Composition::ViewComponentView>();
|
|
258
|
+
if (!viewCompView) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
auto selfView =
|
|
262
|
+
winrt::get_self<winrt::Microsoft::ReactNative::Composition::implementation::ViewComponentView>(viewCompView);
|
|
263
|
+
RECT rc = selfView->getClientRect();
|
|
264
|
+
auto scaleFactor = view.LayoutMetrics().PointScaleFactor;
|
|
265
|
+
if (scaleFactor <= 0) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Anchor in DIPs at the horizontal center of the view's top edge; ShowTooltip re-applies scaleFactor.
|
|
270
|
+
m_pos = {static_cast<float>(rc.left + rc.right) / 2.0f / scaleFactor, static_cast<float>(rc.top) / scaleFactor};
|
|
271
|
+
|
|
272
|
+
m_focusTooltip = true;
|
|
273
|
+
// Claim the single tooltip slot, dismissing any other tracker's pending or visible tooltip.
|
|
274
|
+
m_outer->NotifyShow(this);
|
|
275
|
+
m_timer = winrt::Microsoft::ReactNative::Timer::Create(m_properties.Handle());
|
|
276
|
+
m_timer.Interval(std::chrono::milliseconds(toolTipTimeToShowMs));
|
|
277
|
+
m_timer.Tick({this, &TooltipTracker::OnTick});
|
|
278
|
+
m_timer.Start();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
void TooltipTracker::OnLostFocus(
|
|
282
|
+
const winrt::Windows::Foundation::IInspectable & /*sender*/,
|
|
283
|
+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs & /*args*/) noexcept {
|
|
284
|
+
if (!m_focusTooltip) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
m_focusTooltip = false;
|
|
288
|
+
DestroyTimer();
|
|
289
|
+
DestroyTooltip();
|
|
290
|
+
m_outer->NotifyDismiss(this);
|
|
228
291
|
}
|
|
229
292
|
|
|
230
293
|
void TooltipTracker::OnUnmounted(
|
|
@@ -232,6 +295,7 @@ void TooltipTracker::OnUnmounted(
|
|
|
232
295
|
const winrt::Microsoft::ReactNative::ComponentView &) noexcept {
|
|
233
296
|
DestroyTimer();
|
|
234
297
|
DestroyTooltip();
|
|
298
|
+
m_outer->NotifyDismiss(this);
|
|
235
299
|
}
|
|
236
300
|
|
|
237
301
|
void TooltipTracker::ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept {
|
|
@@ -326,6 +390,22 @@ void TooltipService::StopTracking(const winrt::Microsoft::ReactNative::Component
|
|
|
326
390
|
}
|
|
327
391
|
}
|
|
328
392
|
|
|
393
|
+
void TooltipService::NotifyShow(TooltipTracker *tracker) noexcept {
|
|
394
|
+
if (m_activeTracker == tracker) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (m_activeTracker) {
|
|
398
|
+
m_activeTracker->DismissForExternalRequest();
|
|
399
|
+
}
|
|
400
|
+
m_activeTracker = tracker;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
void TooltipService::NotifyDismiss(TooltipTracker *tracker) noexcept {
|
|
404
|
+
if (m_activeTracker == tracker) {
|
|
405
|
+
m_activeTracker = nullptr;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
329
409
|
static const ReactPropertyId<winrt::Microsoft::ReactNative::ReactNonAbiValue<std::shared_ptr<TooltipService>>>
|
|
330
410
|
&TooltipServicePropertyId() noexcept {
|
|
331
411
|
static const ReactPropertyId<winrt::Microsoft::ReactNative::ReactNonAbiValue<std::shared_ptr<TooltipService>>> prop{
|
|
@@ -27,6 +27,12 @@ struct TooltipTracker {
|
|
|
27
27
|
void OnPointerExited(
|
|
28
28
|
const winrt::Windows::Foundation::IInspectable &sender,
|
|
29
29
|
const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept;
|
|
30
|
+
void OnGotFocus(
|
|
31
|
+
const winrt::Windows::Foundation::IInspectable &sender,
|
|
32
|
+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept;
|
|
33
|
+
void OnLostFocus(
|
|
34
|
+
const winrt::Windows::Foundation::IInspectable &sender,
|
|
35
|
+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept;
|
|
30
36
|
void OnTick(
|
|
31
37
|
const winrt::Windows::Foundation::IInspectable &,
|
|
32
38
|
const winrt::Windows::Foundation::IInspectable &) noexcept;
|
|
@@ -36,6 +42,10 @@ struct TooltipTracker {
|
|
|
36
42
|
|
|
37
43
|
facebook::react::Tag Tag() const noexcept;
|
|
38
44
|
|
|
45
|
+
// Cancel pending dwell timer and close any visible tooltip popup; used by the service when another tracker takes
|
|
46
|
+
// over.
|
|
47
|
+
void DismissForExternalRequest() noexcept;
|
|
48
|
+
|
|
39
49
|
private:
|
|
40
50
|
void ShowTooltip(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
|
|
41
51
|
void DestroyTimer() noexcept;
|
|
@@ -46,6 +56,7 @@ struct TooltipTracker {
|
|
|
46
56
|
::Microsoft::ReactNative::ReactTaggedView m_view;
|
|
47
57
|
winrt::Microsoft::ReactNative::ITimer m_timer;
|
|
48
58
|
HWND m_hwndTip{nullptr};
|
|
59
|
+
bool m_focusTooltip{false};
|
|
49
60
|
winrt::Microsoft::ReactNative::ReactPropertyBag m_properties;
|
|
50
61
|
};
|
|
51
62
|
|
|
@@ -54,12 +65,18 @@ struct TooltipService {
|
|
|
54
65
|
void StartTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
|
|
55
66
|
void StopTracking(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept;
|
|
56
67
|
|
|
68
|
+
// Enforce "only one tooltip visible at a time": dismisses the previously active tracker, if any.
|
|
69
|
+
void NotifyShow(TooltipTracker *tracker) noexcept;
|
|
70
|
+
// Clears the active-tracker slot if it still points at `tracker`.
|
|
71
|
+
void NotifyDismiss(TooltipTracker *tracker) noexcept;
|
|
72
|
+
|
|
57
73
|
static std::shared_ptr<TooltipService> GetCurrent(
|
|
58
74
|
const winrt::Microsoft::ReactNative::ReactPropertyBag &properties) noexcept;
|
|
59
75
|
|
|
60
76
|
private:
|
|
61
77
|
std::vector<std::shared_ptr<TooltipTracker>> m_enteredTrackers;
|
|
62
78
|
std::vector<std::shared_ptr<TooltipTracker>> m_trackers;
|
|
79
|
+
TooltipTracker *m_activeTracker{nullptr};
|
|
63
80
|
winrt::Microsoft::ReactNative::ReactPropertyBag m_properties;
|
|
64
81
|
};
|
|
65
82
|
|
|
@@ -267,7 +267,7 @@ void WindowsTextLayoutManager::GetTextLayout(
|
|
|
267
267
|
attachments.push_back(attachment);
|
|
268
268
|
position += 1;
|
|
269
269
|
} else {
|
|
270
|
-
unsigned int length = static_cast<UINT32>(fragment.string
|
|
270
|
+
unsigned int length = static_cast<UINT32>(Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string));
|
|
271
271
|
DWRITE_TEXT_RANGE range = {position, length};
|
|
272
272
|
TextAttributes attributes = fragment.textAttributes;
|
|
273
273
|
DWRITE_FONT_STYLE fragmentStyle = DWRITE_FONT_STYLE_NORMAL;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<ProjectName>Microsoft.ReactNative</ProjectName>
|
|
11
11
|
<RootNamespace>Microsoft.ReactNative</RootNamespace>
|
|
12
12
|
<DefaultLanguage>en-US</DefaultLanguage>
|
|
13
|
-
<MinimumVisualStudioVersion>
|
|
13
|
+
<MinimumVisualStudioVersion>18.0</MinimumVisualStudioVersion>
|
|
14
14
|
<AppContainerApplication>true</AppContainerApplication>
|
|
15
15
|
<ApplicationType>Windows Store</ApplicationType>
|
|
16
16
|
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
|
|
@@ -96,7 +96,8 @@
|
|
|
96
96
|
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
|
|
97
97
|
<ForcedIncludeFiles>pch.h</ForcedIncludeFiles>
|
|
98
98
|
<WarningLevel>Level4</WarningLevel>
|
|
99
|
-
<AdditionalOptions
|
|
99
|
+
<AdditionalOptions>%(AdditionalOptions) /bigobj /ZH:SHA_256</AdditionalOptions>
|
|
100
|
+
<AdditionalOptions Condition="$(PlatformToolsetVersion)<145">%(AdditionalOptions) /await</AdditionalOptions>
|
|
100
101
|
<AdditionalIncludeDirectories>
|
|
101
102
|
$(FmtDir)\include;
|
|
102
103
|
$(ReactNativeWindowsDir)Microsoft.ReactNative;
|