react-native-windows 0.81.2 → 0.81.4

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 (66) hide show
  1. package/Libraries/Components/Pressable/Pressable.d.ts +8 -0
  2. package/Libraries/Components/Pressable/Pressable.windows.js +21 -2
  3. package/Microsoft.ReactNative/ABIViewManager.cpp +12 -1
  4. package/Microsoft.ReactNative/CompositionSwitcher.idl +16 -9
  5. package/Microsoft.ReactNative/Fabric/ComponentView.cpp +26 -0
  6. package/Microsoft.ReactNative/Fabric/ComponentView.h +2 -0
  7. package/Microsoft.ReactNative/Fabric/Composition/ActivityIndicatorComponentView.cpp +0 -1
  8. package/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.cpp +2 -5
  9. package/Microsoft.ReactNative/Fabric/Composition/CompositionAnnotationProvider.h +1 -4
  10. package/Microsoft.ReactNative/Fabric/Composition/CompositionContextHelper.cpp +15 -0
  11. package/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +65 -32
  12. package/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h +9 -0
  13. package/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +8 -0
  14. package/Microsoft.ReactNative/Fabric/Composition/CompositionRootAutomationProvider.cpp +2 -1
  15. package/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.cpp +4 -7
  16. package/Microsoft.ReactNative/Fabric/Composition/CompositionTextProvider.h +1 -5
  17. package/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.cpp +68 -53
  18. package/Microsoft.ReactNative/Fabric/Composition/CompositionTextRangeProvider.h +1 -5
  19. package/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +98 -15
  20. package/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +10 -3
  21. package/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +73 -10
  22. package/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h +11 -1
  23. package/Microsoft.ReactNative/Fabric/Composition/DebuggingOverlayComponentView.cpp +3 -2
  24. package/Microsoft.ReactNative/Fabric/Composition/ImageComponentView.cpp +0 -1
  25. package/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +609 -4
  26. package/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.h +63 -0
  27. package/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.cpp +8 -0
  28. package/Microsoft.ReactNative/Fabric/Composition/ReactCompositionViewComponentBuilder.h +3 -0
  29. package/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +53 -2
  30. package/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h +8 -1
  31. package/Microsoft.ReactNative/Fabric/Composition/ScrollViewComponentView.cpp +0 -1
  32. package/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp +0 -1
  33. package/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +3 -1
  34. package/Microsoft.ReactNative/Fabric/Composition/Theme.cpp +6 -0
  35. package/Microsoft.ReactNative/Fabric/Composition/TooltipService.cpp +1 -1
  36. package/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +36 -14
  37. package/Microsoft.ReactNative/Fabric/Composition/UnimplementedNativeViewComponentView.cpp +0 -1
  38. package/Microsoft.ReactNative/Fabric/ReactTaggedView.h +1 -1
  39. package/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.h +2 -1
  40. package/Microsoft.ReactNative/IReactViewComponentBuilder.idl +8 -0
  41. package/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +1 -0
  42. package/Microsoft.ReactNative/Modules/ImageViewManagerModule.cpp +41 -15
  43. package/Microsoft.ReactNative/Utils/IcuUtils.cpp +84 -0
  44. package/Microsoft.ReactNative/Utils/IcuUtils.h +42 -0
  45. package/Microsoft.ReactNative.Cxx/StructInfo.h +8 -1
  46. package/Mso/src/dispatchQueue/queueService.cpp +3 -1
  47. package/Mso/src/dispatchQueue/uiScheduler_winrt.cpp +2 -1
  48. package/PropertySheets/Generated/PackageVersion.g.props +3 -3
  49. package/Shared/Networking/OriginPolicyHttpFilter.cpp +2 -1
  50. package/Shared/Shared.vcxitems +1 -0
  51. package/Shared/Shared.vcxitems.filters +1 -0
  52. package/codegen/react/components/rnwcore/ActivityIndicatorView.g.h +14 -0
  53. package/codegen/react/components/rnwcore/AndroidDrawerLayout.g.h +34 -20
  54. package/codegen/react/components/rnwcore/AndroidHorizontalScrollContentView.g.h +14 -0
  55. package/codegen/react/components/rnwcore/AndroidProgressBar.g.h +14 -0
  56. package/codegen/react/components/rnwcore/AndroidSwipeRefreshLayout.g.h +18 -4
  57. package/codegen/react/components/rnwcore/AndroidSwitch.g.h +18 -4
  58. package/codegen/react/components/rnwcore/DebuggingOverlay.g.h +14 -0
  59. package/codegen/react/components/rnwcore/InputAccessory.g.h +14 -0
  60. package/codegen/react/components/rnwcore/ModalHostView.g.h +32 -18
  61. package/codegen/react/components/rnwcore/PullToRefreshView.g.h +18 -4
  62. package/codegen/react/components/rnwcore/SafeAreaView.g.h +14 -0
  63. package/codegen/react/components/rnwcore/Switch.g.h +18 -4
  64. package/codegen/react/components/rnwcore/UnimplementedNativeView.g.h +14 -0
  65. package/codegen/react/components/rnwcore/VirtualView.g.h +48 -6
  66. package/package.json +3 -3
@@ -8,17 +8,44 @@
8
8
  #include <react/renderer/textlayoutmanager/WindowsTextLayoutManager.h>
9
9
 
10
10
  #include <AutoDraw.h>
11
+ #include <Fabric/ReactTaggedView.h>
12
+ #include <Utils/IcuUtils.h>
11
13
  #include <Utils/ValueUtils.h>
12
14
  #include <react/renderer/components/text/ParagraphShadowNode.h>
13
15
  #include <react/renderer/components/text/ParagraphState.h>
14
16
  #include <unicode.h>
15
17
  #include <winrt/Microsoft.ReactNative.Composition.h>
16
- #include "CompositionDynamicAutomationProvider.h"
18
+ #include <winrt/Microsoft.UI.Input.h>
19
+ #include <winrt/Windows.ApplicationModel.DataTransfer.h>
17
20
  #include "CompositionHelpers.h"
21
+ #include "RootComponentView.h"
18
22
  #include "TextDrawing.h"
19
23
 
20
24
  namespace winrt::Microsoft::ReactNative::Composition::implementation {
21
25
 
26
+ // Automatically restores the original DPI of a render target
27
+ struct DpiRestorer {
28
+ ID2D1RenderTarget *renderTarget = nullptr;
29
+ float originalDpiX = 0.0f;
30
+ float originalDpiY = 0.0f;
31
+
32
+ void operator()(ID2D1RenderTarget *) const noexcept {
33
+ if (renderTarget) {
34
+ renderTarget->SetDpi(originalDpiX, originalDpiY);
35
+ }
36
+ }
37
+ };
38
+
39
+ inline std::unique_ptr<ID2D1RenderTarget, DpiRestorer>
40
+ MakeDpiGuard(ID2D1RenderTarget &renderTarget, float newDpiX, float newDpiY) noexcept {
41
+ float originalDpiX, originalDpiY;
42
+ renderTarget.GetDpi(&originalDpiX, &originalDpiY);
43
+ renderTarget.SetDpi(newDpiX, newDpiY);
44
+
45
+ return std::unique_ptr<ID2D1RenderTarget, DpiRestorer>(
46
+ &renderTarget, DpiRestorer{&renderTarget, originalDpiX, originalDpiY});
47
+ }
48
+
22
49
  ParagraphComponentView::ParagraphComponentView(
23
50
  const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
24
51
  facebook::react::Tag tag,
@@ -28,7 +55,8 @@ ParagraphComponentView::ParagraphComponentView(
28
55
  compContext,
29
56
  tag,
30
57
  reactContext,
31
- ComponentViewFeatures::Default & ~ComponentViewFeatures::Background) {}
58
+ // Disable Background (text draws its own) and FocusVisual (selection highlight is the focus indicator)
59
+ ComponentViewFeatures::Default & ~ComponentViewFeatures::Background & ~ComponentViewFeatures::FocusVisual) {}
32
60
 
33
61
  void ParagraphComponentView::MountChildComponentView(
34
62
  const winrt::Microsoft::ReactNative::ComponentView &childComponentView,
@@ -71,6 +99,14 @@ void ParagraphComponentView::updateProps(
71
99
  m_textLayout = nullptr;
72
100
  }
73
101
 
102
+ // Clear selection if text becomes non-selectable
103
+ if (oldViewProps.isSelectable != newViewProps.isSelectable) {
104
+ if (!newViewProps.isSelectable) {
105
+ ClearSelection();
106
+ }
107
+ m_requireRedraw = true;
108
+ }
109
+
74
110
  Super::updateProps(props, oldProps);
75
111
  }
76
112
 
@@ -131,6 +167,128 @@ void ParagraphComponentView::updateTextAlignment(
131
167
  m_textLayout = nullptr;
132
168
  }
133
169
 
170
+ facebook::react::Tag ParagraphComponentView::hitTest(
171
+ facebook::react::Point pt,
172
+ facebook::react::Point &localPt,
173
+ bool ignorePointerEvents) const noexcept {
174
+ facebook::react::Point ptLocal{pt.x - m_layoutMetrics.frame.origin.x, pt.y - m_layoutMetrics.frame.origin.y};
175
+ const auto &props = paragraphProps();
176
+ const auto &vProps = *viewProps();
177
+
178
+ if (props.isSelectable && ptLocal.x >= 0 && ptLocal.x <= m_layoutMetrics.frame.size.width && ptLocal.y >= 0 &&
179
+ ptLocal.y <= m_layoutMetrics.frame.size.height) {
180
+ // claims if pointer events are enabled for this component
181
+ if (ignorePointerEvents || vProps.pointerEvents == facebook::react::PointerEventsMode::Auto ||
182
+ vProps.pointerEvents == facebook::react::PointerEventsMode::BoxOnly) {
183
+ localPt = ptLocal;
184
+ return Tag();
185
+ }
186
+ }
187
+ return Super::hitTest(pt, localPt, ignorePointerEvents);
188
+ }
189
+
190
+ bool ParagraphComponentView::IsTextSelectableAtPoint(facebook::react::Point pt) noexcept {
191
+ // paragraph-level selectable prop is enabled
192
+ const auto &props = paragraphProps();
193
+ if (!props.isSelectable) {
194
+ return false;
195
+ }
196
+
197
+ // Finds which text fragment was hit
198
+ if (m_attributedStringBox.getValue().getFragments().size() && m_textLayout) {
199
+ BOOL isTrailingHit = false;
200
+ BOOL isInside = false;
201
+ DWRITE_HIT_TEST_METRICS metrics;
202
+ winrt::check_hresult(m_textLayout->HitTestPoint(pt.x, pt.y, &isTrailingHit, &isInside, &metrics));
203
+
204
+ if (isInside) {
205
+ uint32_t textPosition = metrics.textPosition;
206
+
207
+ // Finds which fragment contains this text position
208
+ for (auto fragment : m_attributedStringBox.getValue().getFragments()) {
209
+ if (textPosition < fragment.string.length()) {
210
+ return true;
211
+ }
212
+ textPosition -= static_cast<uint32_t>(fragment.string.length());
213
+ }
214
+ }
215
+ }
216
+
217
+ return false;
218
+ }
219
+
220
+ std::optional<int32_t> ParagraphComponentView::GetTextPositionAtPoint(facebook::react::Point pt) noexcept {
221
+ if (!m_textLayout) {
222
+ return std::nullopt;
223
+ }
224
+
225
+ BOOL isTrailingHit = FALSE;
226
+ BOOL isInside = FALSE;
227
+ DWRITE_HIT_TEST_METRICS metrics = {};
228
+
229
+ // Convert screen coordinates to character position
230
+ HRESULT hr = m_textLayout->HitTestPoint(pt.x, pt.y, &isTrailingHit, &isInside, &metrics);
231
+ if (FAILED(hr) || !isInside) {
232
+ return std::nullopt;
233
+ }
234
+
235
+ // Calculates the actual character position
236
+ // If isTrailingHit is true, the point is closer to the trailing edge of the character,
237
+ // so we should return the next character position (for cursor positioning)
238
+ return static_cast<int32_t>(metrics.textPosition + isTrailingHit);
239
+ }
240
+
241
+ std::optional<int32_t> ParagraphComponentView::GetClampedTextPosition(facebook::react::Point pt) noexcept {
242
+ if (!m_textLayout) {
243
+ return std::nullopt;
244
+ }
245
+
246
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
247
+ if (utf16Text.empty()) {
248
+ return std::nullopt;
249
+ }
250
+
251
+ DWRITE_TEXT_METRICS textMetrics;
252
+ if (FAILED(m_textLayout->GetMetrics(&textMetrics))) {
253
+ return std::nullopt;
254
+ }
255
+
256
+ // Clamp the point to the text bounds for hit testing
257
+ const float clampedX = std::max(0.0f, std::min(pt.x, textMetrics.width));
258
+ const float clampedY = std::max(0.0f, std::min(pt.y, textMetrics.height));
259
+
260
+ BOOL isTrailingHit = FALSE;
261
+ BOOL isInside = FALSE;
262
+ DWRITE_HIT_TEST_METRICS metrics = {};
263
+
264
+ HRESULT hr = m_textLayout->HitTestPoint(clampedX, clampedY, &isTrailingHit, &isInside, &metrics);
265
+ if (FAILED(hr)) {
266
+ return std::nullopt;
267
+ }
268
+
269
+ int32_t result = static_cast<int32_t>(metrics.textPosition);
270
+ if (pt.x > textMetrics.width) {
271
+ // Dragging right - go to end of character
272
+ result = static_cast<int32_t>(metrics.textPosition + metrics.length);
273
+ } else if (pt.x < 0) {
274
+ // Dragging left - go to start of character
275
+ result = static_cast<int32_t>(metrics.textPosition);
276
+ } else if (isTrailingHit) {
277
+ // Inside bounds, trailing hit
278
+ result += 1;
279
+ }
280
+
281
+ if (pt.y > textMetrics.height) {
282
+ // Dragging below - select to end of text
283
+ result = static_cast<int32_t>(utf16Text.length());
284
+ } else if (pt.y < 0) {
285
+ // Dragging above - select to start of text
286
+ result = 0;
287
+ }
288
+
289
+ return result;
290
+ }
291
+
134
292
  void ParagraphComponentView::OnRenderingDeviceLost() noexcept {
135
293
  DrawText();
136
294
  }
@@ -263,6 +421,76 @@ void ParagraphComponentView::onThemeChanged() noexcept {
263
421
  }
264
422
 
265
423
  // Renders the text into our composition surface
424
+ void ParagraphComponentView::DrawSelectionHighlight(
425
+ ID2D1RenderTarget &renderTarget,
426
+ float offsetX,
427
+ float offsetY,
428
+ float pointScaleFactor) noexcept {
429
+ if (!m_selectionStart || !m_selectionEnd || !m_textLayout) {
430
+ return;
431
+ }
432
+
433
+ // During drag, selection may not be normalized yet, using min/max for rendering
434
+ const int32_t selStart = std::min(*m_selectionStart, *m_selectionEnd);
435
+ const int32_t selEnd = std::max(*m_selectionStart, *m_selectionEnd);
436
+ if (selEnd <= selStart) {
437
+ return;
438
+ }
439
+
440
+ // Scale offset to match text layout coordinates (same as RenderText)
441
+ const float scaledOffsetX = offsetX / pointScaleFactor;
442
+ const float scaledOffsetY = offsetY / pointScaleFactor;
443
+
444
+ // Set DPI to match text rendering
445
+ const float dpi = pointScaleFactor * 96.0f;
446
+ std::unique_ptr<ID2D1RenderTarget, DpiRestorer> dpiGuard = MakeDpiGuard(renderTarget, dpi, dpi);
447
+
448
+ // Get the hit test metrics for the selected text range
449
+ UINT32 actualCount = 0;
450
+ HRESULT hr = m_textLayout->HitTestTextRange(
451
+ static_cast<UINT32>(selStart),
452
+ static_cast<UINT32>(selEnd - selStart),
453
+ scaledOffsetX,
454
+ scaledOffsetY,
455
+ nullptr,
456
+ 0,
457
+ &actualCount);
458
+
459
+ if (actualCount == 0) {
460
+ return;
461
+ }
462
+
463
+ std::vector<DWRITE_HIT_TEST_METRICS> hitTestMetrics(actualCount);
464
+ hr = m_textLayout->HitTestTextRange(
465
+ static_cast<UINT32>(selStart),
466
+ static_cast<UINT32>(selEnd - selStart),
467
+ scaledOffsetX,
468
+ scaledOffsetY,
469
+ hitTestMetrics.data(),
470
+ actualCount,
471
+ &actualCount);
472
+
473
+ if (FAILED(hr)) {
474
+ return;
475
+ }
476
+
477
+ // TODO: use prop selectionColor if provided
478
+ winrt::com_ptr<ID2D1SolidColorBrush> selectionBrush;
479
+ const D2D1_COLOR_F selectionColor = theme()->D2DPlatformColor("Highlight@40");
480
+ hr = renderTarget.CreateSolidColorBrush(selectionColor, selectionBrush.put());
481
+
482
+ if (FAILED(hr)) {
483
+ return;
484
+ }
485
+
486
+ // Draw rectangles for each hit test metric
487
+ for (UINT32 i = 0; i < actualCount; i++) {
488
+ const auto &metric = hitTestMetrics[i];
489
+ const D2D1_RECT_F rect = {metric.left, metric.top, metric.left + metric.width, metric.top + metric.height};
490
+ renderTarget.FillRectangle(&rect, selectionBrush.get());
491
+ }
492
+ }
493
+
266
494
  void ParagraphComponentView::DrawText() noexcept {
267
495
  if (!m_drawingSurface || theme()->IsEmpty())
268
496
  return;
@@ -281,13 +509,20 @@ void ParagraphComponentView::DrawText() noexcept {
281
509
  viewProps()->backgroundColor ? theme()->D2DColor(*viewProps()->backgroundColor)
282
510
  : D2D1::ColorF(D2D1::ColorF::Black, 0.0f));
283
511
  const auto &props = paragraphProps();
512
+
513
+ // Calculate text offset
514
+ const float textOffsetX = static_cast<float>(offset.x) + m_layoutMetrics.contentInsets.left;
515
+ const float textOffsetY = static_cast<float>(offset.y) + m_layoutMetrics.contentInsets.top;
516
+
517
+ // Draw selection highlight behind text
518
+ DrawSelectionHighlight(*d2dDeviceContext, textOffsetX, textOffsetY, m_layoutMetrics.pointScaleFactor);
519
+
284
520
  RenderText(
285
521
  *d2dDeviceContext,
286
522
  *m_textLayout,
287
523
  m_attributedStringBox.getValue(),
288
524
  props.textAttributes,
289
- {static_cast<float>(offset.x) + m_layoutMetrics.contentInsets.left,
290
- static_cast<float>(offset.y) + m_layoutMetrics.contentInsets.top},
525
+ {textOffsetX, textOffsetY},
291
526
  m_layoutMetrics.pointScaleFactor,
292
527
  *theme());
293
528
 
@@ -299,6 +534,363 @@ void ParagraphComponentView::DrawText() noexcept {
299
534
  }
300
535
  }
301
536
 
537
+ void ParagraphComponentView::ClearSelection() noexcept {
538
+ const bool hadSelection = (m_selectionStart || m_selectionEnd || m_isSelecting);
539
+ m_selectionStart = std::nullopt;
540
+ m_selectionEnd = std::nullopt;
541
+ m_isSelecting = false;
542
+ m_isWordSelecting = false;
543
+ if (hadSelection) {
544
+ // Clears selection highlight
545
+ DrawText();
546
+ }
547
+ }
548
+
549
+ void ParagraphComponentView::OnPointerPressed(
550
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
551
+ // Only handle selection if text is selectable
552
+ const auto &props = paragraphProps();
553
+ if (!props.isSelectable) {
554
+ Super::OnPointerPressed(args);
555
+ return;
556
+ }
557
+
558
+ // Use Tag() to get coordinates in component's local space
559
+ auto pp = args.GetCurrentPoint(static_cast<int32_t>(Tag()));
560
+
561
+ // Ignores right-click
562
+ if (pp.Properties().PointerUpdateKind() ==
563
+ winrt::Microsoft::ReactNative::Composition::Input::PointerUpdateKind::RightButtonPressed) {
564
+ args.Handled(true);
565
+ return;
566
+ }
567
+
568
+ auto position = pp.Position();
569
+
570
+ // GetCurrentPoint(Tag()) returns position relative to component origin
571
+ facebook::react::Point localPt{position.X, position.Y};
572
+
573
+ std::optional<int32_t> charPosition = GetTextPositionAtPoint(localPt);
574
+
575
+ if (charPosition) {
576
+ if (auto root = rootComponentView()) {
577
+ root->ClearCurrentTextSelection();
578
+ }
579
+
580
+ // Check for double-click
581
+ auto now = std::chrono::steady_clock::now();
582
+ auto timeSinceLastClick = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_lastClickTime);
583
+ const UINT doubleClickTime = GetDoubleClickTime();
584
+ const bool isDoubleClick = (timeSinceLastClick.count() < static_cast<long long>(doubleClickTime)) &&
585
+ m_lastClickPosition && (std::abs(*charPosition - *m_lastClickPosition) <= 1);
586
+
587
+ // Update last click tracking
588
+ m_lastClickTime = now;
589
+ m_lastClickPosition = charPosition;
590
+
591
+ if (isDoubleClick) {
592
+ SelectWordAtPosition(*charPosition);
593
+ if (m_selectionStart && m_selectionEnd) {
594
+ m_isWordSelecting = true;
595
+ m_wordAnchorStart = *m_selectionStart;
596
+ m_wordAnchorEnd = *m_selectionEnd;
597
+ m_isSelecting = true;
598
+ CapturePointer(args.Pointer());
599
+ }
600
+ } else {
601
+ // Single-click: start drag selection
602
+ m_selectionStart = charPosition;
603
+ m_selectionEnd = charPosition;
604
+ m_isSelecting = true;
605
+
606
+ // Tracks selection even when the mouse moves outside the component bounds
607
+ CapturePointer(args.Pointer());
608
+ }
609
+
610
+ if (auto root = rootComponentView()) {
611
+ root->SetViewWithTextSelection(*get_strong());
612
+ }
613
+
614
+ // Focuses so we receive onLostFocus when clicking elsewhere
615
+ if (auto root = rootComponentView()) {
616
+ root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
617
+ }
618
+
619
+ args.Handled(true);
620
+ } else {
621
+ ClearSelection();
622
+ m_lastClickPosition = std::nullopt;
623
+ Super::OnPointerPressed(args);
624
+ }
625
+ }
626
+
627
+ void ParagraphComponentView::OnPointerMoved(
628
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
629
+ // Only track movement if we're actively selecting
630
+ if (!m_isSelecting) {
631
+ Super::OnPointerMoved(args);
632
+ return;
633
+ }
634
+
635
+ auto pp = args.GetCurrentPoint(static_cast<int32_t>(Tag()));
636
+ auto position = pp.Position();
637
+
638
+ facebook::react::Point localPt{position.X, position.Y};
639
+ std::optional<int32_t> charPosition = GetClampedTextPosition(localPt);
640
+
641
+ if (charPosition) {
642
+ if (m_isWordSelecting) {
643
+ // Extend selection by whole words
644
+ auto [wordStart, wordEnd] = GetWordBoundariesAtPosition(*charPosition);
645
+
646
+ if (*charPosition < m_wordAnchorStart) {
647
+ m_selectionStart = wordStart;
648
+ m_selectionEnd = m_wordAnchorEnd;
649
+ } else if (*charPosition >= m_wordAnchorEnd) {
650
+ m_selectionStart = m_wordAnchorStart;
651
+ m_selectionEnd = wordEnd;
652
+ } else {
653
+ m_selectionStart = m_wordAnchorStart;
654
+ m_selectionEnd = m_wordAnchorEnd;
655
+ }
656
+ DrawText();
657
+ args.Handled(true);
658
+ } else if (charPosition != m_selectionEnd) {
659
+ m_selectionEnd = charPosition;
660
+ DrawText();
661
+ args.Handled(true);
662
+ }
663
+ }
664
+ }
665
+
666
+ void ParagraphComponentView::OnPointerReleased(
667
+ const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {
668
+ // Check for right-click to show context menu
669
+ auto pp = args.GetCurrentPoint(static_cast<int32_t>(Tag()));
670
+ if (pp.Properties().PointerUpdateKind() ==
671
+ winrt::Microsoft::ReactNative::Composition::Input::PointerUpdateKind::RightButtonReleased) {
672
+ const auto &props = paragraphProps();
673
+ if (props.isSelectable) {
674
+ ShowContextMenu();
675
+ args.Handled(true);
676
+ return;
677
+ }
678
+ }
679
+
680
+ if (!m_isSelecting) {
681
+ Super::OnPointerReleased(args);
682
+ return;
683
+ }
684
+
685
+ m_isSelecting = false;
686
+ m_isWordSelecting = false;
687
+
688
+ ReleasePointerCapture(args.Pointer());
689
+
690
+ if (!m_selectionStart || !m_selectionEnd || *m_selectionStart == *m_selectionEnd) {
691
+ m_selectionStart = std::nullopt;
692
+ m_selectionEnd = std::nullopt;
693
+ } else {
694
+ SetSelection(*m_selectionStart, *m_selectionEnd);
695
+ }
696
+
697
+ args.Handled(true);
698
+ }
699
+
700
+ void ParagraphComponentView::onLostFocus(
701
+ const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept {
702
+ ClearSelection();
703
+
704
+ Super::onLostFocus(args);
705
+ }
706
+
707
+ void ParagraphComponentView::OnPointerCaptureLost() noexcept {
708
+ // Pointer capture was lost stop any active selection drag
709
+ if (m_isSelecting) {
710
+ m_isSelecting = false;
711
+ m_isWordSelecting = false;
712
+
713
+ if (!m_selectionStart || !m_selectionEnd || *m_selectionStart == *m_selectionEnd) {
714
+ m_selectionStart = std::nullopt;
715
+ m_selectionEnd = std::nullopt;
716
+ } else {
717
+ SetSelection(*m_selectionStart, *m_selectionEnd);
718
+ }
719
+ }
720
+
721
+ Super::OnPointerCaptureLost();
722
+ }
723
+
724
+ std::string ParagraphComponentView::GetSelectedText() const noexcept {
725
+ if (!m_selectionStart || !m_selectionEnd) {
726
+ return "";
727
+ }
728
+
729
+ const int32_t selStart = std::min(*m_selectionStart, *m_selectionEnd);
730
+ const int32_t selEnd = std::max(*m_selectionStart, *m_selectionEnd);
731
+
732
+ if (selEnd <= selStart) {
733
+ return "";
734
+ }
735
+
736
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
737
+
738
+ if (selStart >= static_cast<int32_t>(utf16Text.length())) {
739
+ return "";
740
+ }
741
+
742
+ const int32_t clampedEnd = std::min(selEnd, static_cast<int32_t>(utf16Text.length()));
743
+ const std::wstring selectedUtf16 =
744
+ utf16Text.substr(static_cast<size_t>(selStart), static_cast<size_t>(clampedEnd - selStart));
745
+ return ::Microsoft::Common::Unicode::Utf16ToUtf8(selectedUtf16);
746
+ }
747
+
748
+ void ParagraphComponentView::CopySelectionToClipboard() noexcept {
749
+ const std::string selectedText = GetSelectedText();
750
+ if (selectedText.empty()) {
751
+ return;
752
+ }
753
+
754
+ // Convert UTF-8 to wide string for Windows clipboard
755
+ const std::wstring wideText = ::Microsoft::Common::Unicode::Utf8ToUtf16(selectedText);
756
+
757
+ winrt::Windows::ApplicationModel::DataTransfer::DataPackage dataPackage;
758
+ dataPackage.SetText(wideText);
759
+ winrt::Windows::ApplicationModel::DataTransfer::Clipboard::SetContent(dataPackage);
760
+ }
761
+
762
+ std::pair<int32_t, int32_t> ParagraphComponentView::GetWordBoundariesAtPosition(int32_t charPosition) noexcept {
763
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
764
+ const int32_t textLength = static_cast<int32_t>(utf16Text.length());
765
+
766
+ if (utf16Text.empty() || charPosition < 0) {
767
+ return {0, 0};
768
+ }
769
+
770
+ charPosition = std::min(charPosition, textLength - 1);
771
+ if (charPosition < 0) {
772
+ return {0, 0};
773
+ }
774
+
775
+ int32_t wordStart = charPosition;
776
+ int32_t wordEnd = charPosition;
777
+
778
+ ::Microsoft::ReactNative::IcuUtils::WordBreakIterator wordBreaker(utf16Text.c_str(), textLength);
779
+ const bool icuSuccess = wordBreaker.IsValid() && wordBreaker.GetWordBoundaries(charPosition, wordStart, wordEnd);
780
+
781
+ if (!icuSuccess) {
782
+ wordStart = charPosition;
783
+ wordEnd = charPosition;
784
+
785
+ while (wordStart > 0) {
786
+ int32_t prevPos = ::Microsoft::ReactNative::IcuUtils::MoveToPreviousCodePoint(utf16Text.c_str(), wordStart);
787
+ ::Microsoft::ReactNative::IcuUtils::UChar32 prevCp =
788
+ ::Microsoft::ReactNative::IcuUtils::GetCodePointAt(utf16Text.c_str(), textLength, prevPos);
789
+ if (!::Microsoft::ReactNative::IcuUtils::IsAlphanumeric(prevCp)) {
790
+ break;
791
+ }
792
+ wordStart = prevPos;
793
+ }
794
+
795
+ while (wordEnd < textLength) {
796
+ ::Microsoft::ReactNative::IcuUtils::UChar32 cp =
797
+ ::Microsoft::ReactNative::IcuUtils::GetCodePointAt(utf16Text.c_str(), textLength, wordEnd);
798
+ if (!::Microsoft::ReactNative::IcuUtils::IsAlphanumeric(cp)) {
799
+ break;
800
+ }
801
+ wordEnd = ::Microsoft::ReactNative::IcuUtils::MoveToNextCodePoint(utf16Text.c_str(), textLength, wordEnd);
802
+ }
803
+ }
804
+
805
+ return {wordStart, wordEnd};
806
+ }
807
+
808
+ void ParagraphComponentView::SelectWordAtPosition(int32_t charPosition) noexcept {
809
+ auto [wordStart, wordEnd] = GetWordBoundariesAtPosition(charPosition);
810
+
811
+ if (wordEnd > wordStart) {
812
+ SetSelection(wordStart, wordEnd);
813
+ DrawText();
814
+ }
815
+ }
816
+
817
+ void ParagraphComponentView::SetSelection(int32_t start, int32_t end) noexcept {
818
+ m_selectionStart = std::min(start, end);
819
+ m_selectionEnd = std::max(start, end);
820
+ }
821
+
822
+ void ParagraphComponentView::ShowContextMenu() noexcept {
823
+ HMENU menu = CreatePopupMenu();
824
+ if (!menu) {
825
+ return;
826
+ }
827
+
828
+ const bool hasSelection = (m_selectionStart && m_selectionEnd && *m_selectionStart != *m_selectionEnd);
829
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
830
+ const bool hasText = !utf16Text.empty();
831
+
832
+ // Add menu items (1 = Copy, 2 = Select All)
833
+ AppendMenuW(menu, MF_STRING | (hasSelection ? 0 : MF_GRAYED), 1, L"Copy");
834
+ AppendMenuW(menu, MF_STRING | (hasText ? 0 : MF_GRAYED), 2, L"Select All");
835
+
836
+ // Get cursor position for menu placement
837
+ POINT cursorPos;
838
+ GetCursorPos(&cursorPos);
839
+
840
+ const HWND hwnd = GetActiveWindow();
841
+
842
+ const int cmd = TrackPopupMenu(
843
+ menu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY, cursorPos.x, cursorPos.y, 0, hwnd, NULL);
844
+
845
+ if (cmd == 1) {
846
+ // Copy
847
+ CopySelectionToClipboard();
848
+ } else if (cmd == 2) {
849
+ SetSelection(0, static_cast<int32_t>(utf16Text.length()));
850
+ DrawText();
851
+ }
852
+
853
+ DestroyMenu(menu);
854
+ }
855
+
856
+ void ParagraphComponentView::OnKeyDown(
857
+ const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
858
+ const bool isCtrlDown =
859
+ (args.KeyboardSource().GetKeyState(winrt::Windows::System::VirtualKey::Control) &
860
+ winrt::Microsoft::UI::Input::VirtualKeyStates::Down) == winrt::Microsoft::UI::Input::VirtualKeyStates::Down;
861
+
862
+ // Handle Ctrl+C for copy
863
+ if (isCtrlDown && args.Key() == winrt::Windows::System::VirtualKey::C) {
864
+ if (m_selectionStart && m_selectionEnd && *m_selectionStart != *m_selectionEnd) {
865
+ CopySelectionToClipboard();
866
+ args.Handled(true);
867
+ return;
868
+ }
869
+ }
870
+
871
+ // Handle Ctrl+A for select all
872
+ if (isCtrlDown && args.Key() == winrt::Windows::System::VirtualKey::A) {
873
+ const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText(m_attributedStringBox)};
874
+ if (!utf16Text.empty()) {
875
+ if (auto root = rootComponentView()) {
876
+ root->ClearCurrentTextSelection();
877
+ }
878
+
879
+ SetSelection(0, static_cast<int32_t>(utf16Text.length()));
880
+
881
+ if (auto root = rootComponentView()) {
882
+ root->SetViewWithTextSelection(*get_strong());
883
+ }
884
+
885
+ DrawText();
886
+ args.Handled(true);
887
+ return;
888
+ }
889
+ }
890
+
891
+ Super::OnKeyDown(args);
892
+ }
893
+
302
894
  std::string ParagraphComponentView::DefaultControlType() const noexcept {
303
895
  return "text";
304
896
  }
@@ -307,6 +899,19 @@ std::string ParagraphComponentView::DefaultAccessibleName() const noexcept {
307
899
  return m_attributedStringBox.getValue().getString();
308
900
  }
309
901
 
902
+ bool ParagraphComponentView::focusable() const noexcept {
903
+ // Text is focusable when it's selectable or when explicitly marked as focusable via props
904
+ return paragraphProps().isSelectable || viewProps()->focusable;
905
+ }
906
+
907
+ std::pair<facebook::react::Cursor, HCURSOR> ParagraphComponentView::cursor() const noexcept {
908
+ // Returns I-beam cursor for selectable text
909
+ if (paragraphProps().isSelectable) {
910
+ return {facebook::react::Cursor::Text, nullptr};
911
+ }
912
+ return Super::cursor();
913
+ }
914
+
310
915
  winrt::Microsoft::ReactNative::ComponentView ParagraphComponentView::Create(
311
916
  const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
312
917
  facebook::react::Tag tag,