diff --git a/change/react-native-windows-1efdb03b-a0af-4ffa-a274-a0c43f757681.json b/change/react-native-windows-1efdb03b-a0af-4ffa-a274-a0c43f757681.json new file mode 100644 index 00000000000..a4bd58f9a3b --- /dev/null +++ b/change/react-native-windows-1efdb03b-a0af-4ffa-a274-a0c43f757681.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "updated ellipsis to respect tail and clip behaviour , for head , middle follow defaulty tail behaviour", + "packageName": "react-native-windows", + "email": "74712637+iamAbhi-916@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextHitTestTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextHitTestTest.test.ts.snap index a31407e3fde..5b0d72486b1 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextHitTestTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextHitTestTest.test.ts.snap @@ -342,8 +342,8 @@ exports[`LegacyTextHitTestTest WrappedLTRInRTLFlowEdgeCaseNotPressable 1`] = ` "AutomationId": "pressed-state", "ControlType": 50020, "LocalizedControlType": "text", - "Name": "Pressed: 1 times.", - "TextRangePattern.GetText": "Pressed: 1 times.", + "Name": "Pressed: 0 times.", + "TextRangePattern.GetText": "Pressed: 0 times.", }, "Component Tree": { "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextInputTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextInputTest.test.ts.snap index 6e621a9668c..dc4c6e56024 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextInputTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/LegacyTextInputTest.test.ts.snap @@ -162,7 +162,7 @@ onFocus "Visual Tree": { "Comment": "textinput-log", "Offset": "0, 0, 0", - "Size": "998, 839", + "Size": "998, 745", "Visual Type": "SpriteVisual", }, } diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap index 9d526f39324..6fb1eb024cd 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/TextComponentTest.test.ts.snap @@ -18,7 +18,7 @@ exports[`Text Tests Padding can be added to Text 1`] = ` "Visual Tree": { "Comment": "text-padding", "Offset": "0, 0, 0", - "Size": "916, 39", + "Size": "916, 40", "Visual Type": "SpriteVisual", }, } @@ -234,7 +234,7 @@ exports[`Text Tests Text can be restricted to one line 1`] = ` "Visual Tree": { "Comment": "text-one-line", "Offset": "0, 0, 0", - "Size": "300, 19", + "Size": "300, 20", "Visual Type": "SpriteVisual", }, } @@ -258,7 +258,7 @@ exports[`Text Tests Text can be selectable 1`] = ` "Visual Tree": { "Comment": "text-selectable", "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, } @@ -282,7 +282,7 @@ exports[`Text Tests Text can have a color 1`] = ` "Visual Tree": { "Comment": "text-color", "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, } @@ -306,7 +306,7 @@ exports[`Text Tests Text can have a customized selection color 1`] = ` "Visual Tree": { "Comment": "text-selection-color", "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, } @@ -330,7 +330,7 @@ exports[`Text Tests Text can have a size 1`] = ` "Visual Tree": { "Comment": "text-size", "Offset": "0, 0, 0", - "Size": "916, 31", + "Size": "916, 32", "Visual Type": "SpriteVisual", }, } @@ -482,12 +482,12 @@ exports[`Text Tests Text can have advanced borders 1`] = ` }, { "Offset": "0, 38, 0", - "Size": "916, 39", + "Size": "916, 40", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 39", + "Size": "916, 40", "Visual Type": "SpriteVisual", "__Children": [ { @@ -523,7 +523,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(0, 0, 255, 255)", }, "Offset": "-10, 20, 0", - "Size": "10, 13", + "Size": "10, 14", "Visual Type": "SpriteVisual", }, { @@ -559,7 +559,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(0, 0, 255, 255)", }, "Offset": "0, 22, 0", - "Size": "20, 9", + "Size": "20, 10", "Visual Type": "SpriteVisual", }, ], @@ -568,12 +568,12 @@ exports[`Text Tests Text can have advanced borders 1`] = ` }, { "Offset": "0, 77, 0", - "Size": "916, 22", + "Size": "916, 21", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 22", + "Size": "916, 21", "Visual Type": "SpriteVisual", "__Children": [ { @@ -609,7 +609,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(0, 128, 0, 255)", }, "Offset": "-1, 4, 0", - "Size": "1, 14", + "Size": "1, 13", "Visual Type": "SpriteVisual", }, { @@ -645,7 +645,7 @@ exports[`Text Tests Text can have advanced borders 1`] = ` "Color": "rgba(0, 128, 0, 255)", }, "Offset": "0, 4, 0", - "Size": "1, 14", + "Size": "1, 13", "Visual Type": "SpriteVisual", }, ], @@ -799,12 +799,12 @@ exports[`Text Tests Text can have borders 1`] = ` }, { "Offset": "100, 127, 0", - "Size": "716, 138", + "Size": "716, 139", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "716, 138", + "Size": "716, 139", "Visual Type": "SpriteVisual", "__Children": [ { @@ -886,7 +886,7 @@ exports[`Text Tests Text can have decoration lines: Solid Line Through 1`] = ` "Visual Tree": { "Comment": "text-decoration-solid-linethru", "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, } @@ -1042,24 +1042,24 @@ exports[`Text Tests Text can have nested views 1`] = ` "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, ], }, { "Offset": "0, 18, 0", - "Size": "916, 24", + "Size": "916, 23", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 24", + "Size": "916, 23", "Visual Type": "SpriteVisual", "__Children": [ { @@ -1105,7 +1105,7 @@ exports[`Text Tests Text can have shadows 1`] = ` "Visual Tree": { "Comment": "text-shadow", "Offset": "0, 0, 0", - "Size": "916, 28", + "Size": "916, 27", "Visual Type": "SpriteVisual", }, } @@ -1129,7 +1129,7 @@ exports[`Text Tests Text can wrap 1`] = ` "Visual Tree": { "Comment": "text-wrap", "Offset": "0, 0, 0", - "Size": "300, 38", + "Size": "300, 39", "Visual Type": "SpriteVisual", }, } @@ -1228,12 +1228,12 @@ exports[`Text Tests Texts can clip inline View/Images 1`] = ` "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 19", + "Size": "916, 20", "Visual Type": "SpriteVisual", }, ], @@ -1286,12 +1286,12 @@ exports[`Text Tests Texts can clip inline View/Images 1`] = ` }, { "Offset": "0, 103, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", "__Children": [ { "Offset": "0, 0, 0", - "Size": "916, 20", + "Size": "916, 19", "Visual Type": "SpriteVisual", }, ], diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp index 00130079577..c2093ef9bce 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp @@ -63,6 +63,10 @@ void ParagraphComponentView::updateProps( updateTextAlignment(newViewProps.textAttributes.alignment); } + if (oldViewProps.paragraphAttributes.ellipsizeMode != newViewProps.paragraphAttributes.ellipsizeMode) { + m_textLayout = nullptr; + } + if (oldViewProps.paragraphAttributes.adjustsFontSizeToFit != newViewProps.paragraphAttributes.adjustsFontSizeToFit) { m_requireRedraw = true; } @@ -100,7 +104,7 @@ void ParagraphComponentView::FinalizeUpdates( facebook::react::SharedViewEventEmitter ParagraphComponentView::eventEmitterAtPoint( facebook::react::Point pt) noexcept { - if (m_attributedStringBox.getValue().getFragments().size()) { + if (m_attributedStringBox.getValue().getFragments().size() && m_textLayout) { BOOL isTrailingHit = false; BOOL isInside = false; DWRITE_HIT_TEST_METRICS metrics; @@ -123,35 +127,8 @@ facebook::react::SharedViewEventEmitter ParagraphComponentView::eventEmitterAtPo void ParagraphComponentView::updateTextAlignment( const std::optional &fbAlignment) noexcept { + // Reset text layout to force recreation with new alignment m_textLayout = nullptr; - if (!m_textLayout) - return; - - DWRITE_TEXT_ALIGNMENT alignment = DWRITE_TEXT_ALIGNMENT_LEADING; - if (fbAlignment) { - switch (*fbAlignment) { - case facebook::react::TextAlignment::Center: - alignment = DWRITE_TEXT_ALIGNMENT_CENTER; - break; - case facebook::react::TextAlignment::Justified: - alignment = DWRITE_TEXT_ALIGNMENT_JUSTIFIED; - break; - case facebook::react::TextAlignment::Left: - alignment = DWRITE_TEXT_ALIGNMENT_LEADING; - break; - case facebook::react::TextAlignment::Right: - alignment = DWRITE_TEXT_ALIGNMENT_TRAILING; - break; - // TODO use LTR values - case facebook::react::TextAlignment::Natural: - alignment = DWRITE_TEXT_ALIGNMENT_LEADING; - break; - default: - assert(false); - } - } - // TODO - // m_textFormat->SetTextAlignment(alignment); } void ParagraphComponentView::OnRenderingDeviceLost() noexcept { @@ -163,7 +140,6 @@ void ParagraphComponentView::updateVisualBrush() noexcept { // TODO // updateTextAlignment(paragraphProps.textAttributes.alignment); - if (!m_textLayout) { facebook::react::LayoutConstraints constraints; constraints.maximumSize.width = @@ -174,6 +150,35 @@ void ParagraphComponentView::updateVisualBrush() noexcept { facebook::react::WindowsTextLayoutManager::GetTextLayout( m_attributedStringBox, m_paragraphAttributes, constraints, m_textLayout); + // Apply text alignment after creating the text layout + if (m_textLayout) { + const auto &props = paragraphProps(); + DWRITE_TEXT_ALIGNMENT alignment = DWRITE_TEXT_ALIGNMENT_LEADING; + if (props.textAttributes.alignment) { + switch (*props.textAttributes.alignment) { + case facebook::react::TextAlignment::Center: + alignment = DWRITE_TEXT_ALIGNMENT_CENTER; + break; + case facebook::react::TextAlignment::Justified: + alignment = DWRITE_TEXT_ALIGNMENT_JUSTIFIED; + break; + case facebook::react::TextAlignment::Left: + alignment = DWRITE_TEXT_ALIGNMENT_LEADING; + break; + case facebook::react::TextAlignment::Right: + alignment = DWRITE_TEXT_ALIGNMENT_TRAILING; + break; + case facebook::react::TextAlignment::Natural: + alignment = DWRITE_TEXT_ALIGNMENT_LEADING; + break; + default: + alignment = DWRITE_TEXT_ALIGNMENT_LEADING; + break; + } + } + winrt::check_hresult(m_textLayout->SetTextAlignment(alignment)); + } + requireNewBrush = true; } @@ -275,9 +280,7 @@ void ParagraphComponentView::DrawText() noexcept { d2dDeviceContext->Clear( viewProps()->backgroundColor ? theme()->D2DColor(*viewProps()->backgroundColor) : D2D1::ColorF(D2D1::ColorF::Black, 0.0f)); - const auto &props = paragraphProps(); - RenderText( *d2dDeviceContext, *m_textLayout, 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 332001f604a..b383fa72449 100644 --- a/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/platform/react/renderer/textlayoutmanager/WindowsTextLayoutManager.cpp @@ -160,7 +160,6 @@ void WindowsTextLayoutManager::GetTextLayout( // Get text with Object Replacement Characters for attachments auto str = GetTransformedText(attributedStringBox); - winrt::check_hresult(Microsoft::ReactNative::DWriteFactory()->CreateTextLayout( str.c_str(), // The string to be laid out and formatted. static_cast(str.size()), // The length of the string. @@ -170,6 +169,39 @@ void WindowsTextLayoutManager::GetTextLayout( spTextLayout.put() // The IDWriteTextLayout interface pointer. )); + // Apply max width constraint and ellipsis trimming to ensure consistency with rendering + DWRITE_TEXT_METRICS metrics; + winrt::check_hresult(spTextLayout->GetMetrics(&metrics)); + + if (metrics.width > size.width) { + spTextLayout->SetMaxWidth(size.width); + } + + // Apply DWRITE_TRIMMING for ellipsizeMode + DWRITE_TRIMMING trimming = {}; + winrt::com_ptr ellipsisSign; + + switch (paragraphAttributes.ellipsizeMode) { + case facebook::react::EllipsizeMode::Tail: + trimming.granularity = DWRITE_TRIMMING_GRANULARITY_CHARACTER; + break; + case facebook::react::EllipsizeMode::Clip: + trimming.granularity = DWRITE_TRIMMING_GRANULARITY_NONE; + break; + default: + trimming.granularity = DWRITE_TRIMMING_GRANULARITY_CHARACTER; // Default to tail behavior + break; + } + + // Use DWriteFactory to create the ellipsis trimming sign + if (trimming.granularity != DWRITE_TRIMMING_GRANULARITY_NONE) { + auto dwriteFactory = Microsoft::ReactNative::DWriteFactory(); + HRESULT hr = dwriteFactory->CreateEllipsisTrimmingSign(spTextLayout.get(), ellipsisSign.put()); + if (SUCCEEDED(hr)) { + spTextLayout->SetTrimming(&trimming, ellipsisSign.get()); + } + } + // Calculate positions for attachments and set inline objects unsigned int position = 0; for (const auto &fragment : fragments) {