@@ -539,6 +539,7 @@ void ParagraphComponentView::ClearSelection() noexcept {
539539 m_selectionStart = std::nullopt ;
540540 m_selectionEnd = std::nullopt ;
541541 m_isSelecting = false ;
542+ m_isWordSelecting = false ;
542543 if (hadSelection) {
543544 // Clears selection highlight
544545 DrawText ();
@@ -589,7 +590,13 @@ void ParagraphComponentView::OnPointerPressed(
589590
590591 if (isDoubleClick) {
591592 SelectWordAtPosition (*charPosition);
592- m_isSelecting = false ;
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+ }
593600 } else {
594601 // Single-click: start drag selection
595602 m_selectionStart = charPosition;
@@ -631,10 +638,28 @@ void ParagraphComponentView::OnPointerMoved(
631638 facebook::react::Point localPt{position.X , position.Y };
632639 std::optional<int32_t > charPosition = GetClampedTextPosition (localPt);
633640
634- if (charPosition && charPosition != m_selectionEnd) {
635- m_selectionEnd = charPosition;
636- DrawText ();
637- args.Handled (true );
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+ }
638663 }
639664}
640665
@@ -658,6 +683,7 @@ void ParagraphComponentView::OnPointerReleased(
658683 }
659684
660685 m_isSelecting = false ;
686+ m_isWordSelecting = false ;
661687
662688 ReleasePointerCapture (args.Pointer ());
663689
@@ -682,6 +708,7 @@ void ParagraphComponentView::OnPointerCaptureLost() noexcept {
682708 // Pointer capture was lost stop any active selection drag
683709 if (m_isSelecting) {
684710 m_isSelecting = false ;
711+ m_isWordSelecting = false ;
685712
686713 if (!m_selectionStart || !m_selectionEnd || *m_selectionStart == *m_selectionEnd) {
687714 m_selectionStart = std::nullopt ;
@@ -732,12 +759,17 @@ void ParagraphComponentView::CopySelectionToClipboard() noexcept {
732759 winrt::Windows::ApplicationModel::DataTransfer::Clipboard::SetContent (dataPackage);
733760}
734761
735- void ParagraphComponentView::SelectWordAtPosition (int32_t charPosition) noexcept {
762+ std::pair< int32_t , int32_t > ParagraphComponentView::GetWordBoundariesAtPosition (int32_t charPosition) noexcept {
736763 const std::wstring utf16Text{facebook::react::WindowsTextLayoutManager::GetTransformedText (m_attributedStringBox)};
737764 const int32_t textLength = static_cast <int32_t >(utf16Text.length ());
738765
739- if (utf16Text.empty () || charPosition < 0 || charPosition >= textLength) {
740- return ;
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 };
741773 }
742774
743775 int32_t wordStart = charPosition;
@@ -770,6 +802,12 @@ void ParagraphComponentView::SelectWordAtPosition(int32_t charPosition) noexcept
770802 }
771803 }
772804
805+ return {wordStart, wordEnd};
806+ }
807+
808+ void ParagraphComponentView::SelectWordAtPosition (int32_t charPosition) noexcept {
809+ auto [wordStart, wordEnd] = GetWordBoundariesAtPosition (charPosition);
810+
773811 if (wordEnd > wordStart) {
774812 SetSelection (wordStart, wordEnd);
775813 DrawText ();
0 commit comments