diff --git a/change/react-native-windows-c3de21ab-adb1-4f1d-a290-4f0c29f087ef.json b/change/react-native-windows-c3de21ab-adb1-4f1d-a290-4f0c29f087ef.json new file mode 100644 index 00000000000..6a20d5e36d7 --- /dev/null +++ b/change/react-native-windows-c3de21ab-adb1-4f1d-a290-4f0c29f087ef.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "fix: Unicode Text length Calculation", + "packageName": "react-native-windows", + "email": "66076509+vineethkuttan@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Common/unicode.cpp b/vnext/Common/unicode.cpp index 4376367a3df..d3cd7c2fab9 100644 --- a/vnext/Common/unicode.cpp +++ b/vnext/Common/unicode.cpp @@ -93,6 +93,42 @@ std::wstring Utf8ToUtf16(const std::string &utf8) { return Utf8ToUtf16(utf8.c_str(), utf8.length()); } +size_t Utf8ToUtf16Length(const char *utf8, size_t utf8Len) { + if (utf8Len == 0) { + return 0; + } + + if (utf8Len > static_cast((std::numeric_limits::max)())) { + throw std::overflow_error("Length of input string to Utf8ToUtf16Length() must fit into an int."); + } + + const int utf8Length = static_cast(utf8Len); + + constexpr DWORD flags = 0; + + const int utf16Length = ::MultiByteToWideChar( + CP_UTF8, // Source string is in UTF-8. + flags, // Conversion flags. + utf8, // Source UTF-8 string pointer. + utf8Length, // Length of the source UTF-8 string, in chars. + nullptr, // Do not convert, just request the size. + 0 // Request size of destination buffer, in wchar_ts. + ); + + if (utf16Length == 0) { + throw UnicodeConversionException( + "Cannot get result string length when converting from UTF-8 to UTF-16 " + "(MultiByteToWideChar failed).", + GetLastError()); + } + + return static_cast(utf16Length); +} + +size_t Utf8ToUtf16Length(const std::string &utf8) { + return Utf8ToUtf16Length(utf8.c_str(), utf8.length()); +} + #if _HAS_CXX17 std::wstring Utf8ToUtf16(const std::string_view &utf8) { return Utf8ToUtf16(utf8.data(), utf8.length()); diff --git a/vnext/Common/unicode.h b/vnext/Common/unicode.h index 60067ce456c..6cb666daeba 100644 --- a/vnext/Common/unicode.h +++ b/vnext/Common/unicode.h @@ -55,6 +55,14 @@ class UnicodeConversionException : public std::runtime_error { /* (4) */ std::wstring Utf8ToUtf16(const std::string_view &utf8); #endif +// The following functions return the length of the UTF-16 string that would +// result from converting the input UTF-8 string, without actually performing +// the conversion or allocating a temporary std::wstring. This is useful in +// hot paths where only the length is needed (e.g. DirectWrite text ranges). +// +size_t Utf8ToUtf16Length(const char *utf8, size_t utf8Len); +size_t Utf8ToUtf16Length(const std::string &utf8); + // The following functions convert UTF-16BE strings to UTF-8 strings. Their // behaviors mirror those of the above Utf8ToUtf16 functions. // diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp index 63de15cb3af..aff556bcc85 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp @@ -153,11 +153,12 @@ facebook::react::SharedViewEventEmitter ParagraphComponentView::eventEmitterAtPo uint32_t textPosition = metrics.textPosition; for (auto fragment : m_attributedStringBox.getValue().getFragments()) { - if (textPosition < fragment.string.length()) { + uint32_t utf16Length = static_cast(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string)); + if (textPosition < utf16Length) { return std::static_pointer_cast( fragment.parentShadowView.eventEmitter); } - textPosition -= static_cast(fragment.string.length()); + textPosition -= utf16Length; } } } @@ -210,10 +211,11 @@ bool ParagraphComponentView::IsTextSelectableAtPoint(facebook::react::Point pt) // Finds which fragment contains this text position for (auto fragment : m_attributedStringBox.getValue().getFragments()) { - if (textPosition < fragment.string.length()) { + uint32_t utf16Length = static_cast(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string)); + if (textPosition < utf16Length) { return true; } - textPosition -= static_cast(fragment.string.length()); + textPosition -= utf16Length; } } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextDrawing.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextDrawing.cpp index 29a33dff048..3dee7020fb6 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextDrawing.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextDrawing.cpp @@ -66,7 +66,7 @@ void RenderText( unsigned int position = 0; unsigned int length = 0; for (auto fragment : attributedString.getFragments()) { - length = static_cast(fragment.string.length()); + length = static_cast(::Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string)); DWRITE_TEXT_RANGE range = {position, length}; if (fragment.textAttributes.foregroundColor && (fragment.textAttributes.foregroundColor != textAttributes.foregroundColor) || diff --git a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp index 1a9afbf97d8..a63e0e2dca8 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp @@ -262,7 +262,7 @@ void WindowsTextLayoutManager::GetTextLayout( attachments.push_back(attachment); position += 1; } else { - unsigned int length = static_cast(fragment.string.length()); + unsigned int length = static_cast(Microsoft::Common::Unicode::Utf8ToUtf16Length(fragment.string)); DWRITE_TEXT_RANGE range = {position, length}; TextAttributes attributes = fragment.textAttributes; DWRITE_FONT_STYLE fragmentStyle = DWRITE_FONT_STYLE_NORMAL;