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.
Files changed (40) hide show
  1. package/Common/unicode.cpp +36 -0
  2. package/Common/unicode.h +8 -0
  3. package/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +5 -0
  4. package/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +106 -24
  5. package/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +11 -0
  6. package/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp +4 -0
  7. package/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.h +1 -0
  8. package/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +6 -4
  9. package/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +28 -6
  10. package/Microsoft.ReactNative/Fabric/Composition/TextDrawing.cpp +1 -1
  11. package/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp +80 -0
  12. package/Microsoft.ReactNative/Fabric/Composition/TooltipService.h +17 -0
  13. package/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp +1 -1
  14. package/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +3 -2
  15. package/Microsoft.ReactNative/Utils/LocalBundleReader.cpp +33 -25
  16. package/Microsoft.ReactNative/Utils/LocalBundleReader.h +5 -3
  17. package/Microsoft.ReactNative/Utils/UwpScriptStore.cpp +2 -2
  18. package/Microsoft.ReactNative/Utils/UwpScriptStore.h +3 -2
  19. package/Microsoft.ReactNative.Managed/Microsoft.ReactNative.Managed.csproj +3 -3
  20. package/Microsoft.ReactNative.Managed.CodeGen/Microsoft.ReactNative.Managed.CodeGen.csproj +10 -10
  21. package/Microsoft.ReactNative.Managed.CodeGen/Properties/PublishProfiles/DeployAsTool-Debug.pubxml +1 -1
  22. package/Microsoft.ReactNative.Managed.CodeGen/Properties/PublishProfiles/DeployAsTool-Release.pubxml +1 -1
  23. package/PropertySheets/Generated/PackageVersion.g.props +2 -2
  24. package/PropertySheets/JSEngine.props +1 -1
  25. package/PropertySheets/React.Cpp.props +4 -3
  26. package/PropertySheets/WinUI.props +2 -2
  27. package/Scripts/JustMyXaml.ps1 +8 -8
  28. package/Scripts/UnitTest.ps1 +3 -1
  29. package/Scripts/rnw-dependencies.ps1 +15 -16
  30. package/Shared/DevSupportManager.cpp +55 -48
  31. package/Shared/DevSupportManager.h +0 -1
  32. package/Shared/Modules/IWebSocketModuleContentHandler.h +15 -0
  33. package/Shared/Modules/WebSocketModule.cpp +8 -3
  34. package/Shared/Networking/DefaultBlobResource.cpp +37 -0
  35. package/Shared/Networking/DefaultBlobResource.h +12 -0
  36. package/Shared/Networking/WinRTWebSocketResource.h +5 -1
  37. package/just-task.js +13 -2
  38. package/package.json +6 -6
  39. package/templates/cpp-app/windows/MyApp/MyApp.vcxproj +1 -1
  40. package/templates/cpp-lib/windows/MyLib/MyLib.vcxproj +1 -1
@@ -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 = pointerId == 1;
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
- size_t index = 0;
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
- facebook::react::PointerEvent pointerEvent = CreatePointerEventFromActiveTouch(activeTouch, eventType);
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 && activeTouch.shouldLeaveWhenReleased) ||
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, &activeTouch, eventType, &pointerEvent](
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
- activeTouch.eventEmitter->onPointerDown(pointerEvent);
1791
+ changedTouch->eventEmitter->onPointerDown(pointerEvent);
1710
1792
  break;
1711
1793
  case TouchEventType::Move: {
1712
- activeTouch.eventEmitter->onPointerMove(pointerEvent);
1794
+ changedTouch->eventEmitter->onPointerMove(pointerEvent);
1713
1795
  break;
1714
1796
  }
1715
1797
  case TouchEventType::End:
1716
- activeTouch.eventEmitter->onPointerUp(pointerEvent);
1798
+ changedTouch->eventEmitter->onPointerUp(pointerEvent);
1717
1799
  if (pointerEvent.isPrimary && pointerEvent.button == 0) {
1718
- if (IsPointerWithinInitialTree(activeTouch)) {
1719
- activeTouch.eventEmitter->onClick(pointerEvent);
1800
+ if (IsPointerWithinInitialTree(*changedTouch)) {
1801
+ changedTouch->eventEmitter->onClick(pointerEvent);
1720
1802
  }
1721
- } /* else if (IsPointerWithinInitialTree(activeTouch)) {
1722
- activeTouch.eventEmitter->onAuxClick(pointerEvent);
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
- activeTouch.eventEmitter->onPointerCancel(pointerEvent);
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
- if (textPosition < fragment.string.length()) {
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 -= static_cast<uint32_t>(fragment.string.length());
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
- if (textPosition < fragment.string.length()) {
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 -= static_cast<uint32_t>(fragment.string.length());
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
- auto scrollPosition = m_scrollVisual.ScrollPosition();
853
- m_verticalScrollbarComponent->ContentOffset(scrollPosition);
854
- m_horizontalScrollbarComponent->ContentOffset(scrollPosition);
855
-
856
- m_state->updateState([scrollPosition](const facebook::react::ScrollViewShadowNode::ConcreteState::Data &data) {
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 = {scrollPosition.x, scrollPosition.y};
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.length());
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.length());
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>17.0</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>/await %(AdditionalOptions) /bigobj /ZH:SHA_256</AdditionalOptions>
99
+ <AdditionalOptions>%(AdditionalOptions) /bigobj /ZH:SHA_256</AdditionalOptions>
100
+ <AdditionalOptions Condition="$(PlatformToolsetVersion)&lt;145">%(AdditionalOptions) /await</AdditionalOptions>
100
101
  <AdditionalIncludeDirectories>
101
102
  $(FmtDir)\include;
102
103
  $(ReactNativeWindowsDir)Microsoft.ReactNative;