From 34356c01c723de78b1506a424489aeb38d94555a Mon Sep 17 00:00:00 2001 From: Eric Rozell Date: Fri, 17 Sep 2021 17:52:41 -0400 Subject: [PATCH] Introduces TextVisitor to consolidate text traversal algorithms (#8454) * Initial implementation of TextVisitor We have 3 different features that require a DFS traversal of the text tree: 1. The textTransform algorithm needs to recurse the undefined paths of the Text sub-tree where the prop value changes to update. 2. The backgroundColor algorithm needs to recurse the entire text tree to re-calculate highlighters any time the text changes or backgroundColor / foregroundColor props change within the tree. 3. The WIP hit test algorithm needs to recurse the text tree to identify which span is pressed. Additionally, we have 4 "upward" traversal algorithms: 1. When a RCTRawText changes it's text prop, we need to send an accessibility notification 2. When a RCTRawText changes, we need to re-apply text transforms. 3. When a background or foregroundColor changes, we need to notify each parent that it may require a highlighter for an optimization that skips calculating highlighters for text trees without foreground or background colors. 4. In the WIP hit test algorithm, we need to notify parent nodes that a nested Text component is pressable for the optimization that selectively skips traversal of non-pressable sub-trees Each of these algorithms follow the same pattern, so reduce the potential for bugs, I'd like to consolidate these algorithms to use a visitor pattern. * Change files * Move highlighter logic to visitor pattern * Fixes a couple bugs and adds VisitChildren method to share logic * Split text visitors into individual files * Fixes issue with inherited text transforms Previously, if the child of a virtual text node with undefined textTransform was appended, and that virtual text node had a parent with a defined textTransform, the textTransform value would not inherit properly. Similarly, if a virtual text node switched from defined to undefined for textTransform prop, it would not inherit the correct value and update children accordingly either. Fixes #8460 * Adds TextPropertyChangedParentVisitor for Text tree traversals when text or highlights are updated * Removes irrelevant headers and fixes namespaces * Refactor TextVisitor to eliminate need for `VisitExtensionText' --- ...-f7be41bd-666d-4885-aa40-4c801dad1528.json | 7 + .../Microsoft.ReactNative.vcxproj | 14 ++ .../Microsoft.ReactNative.vcxproj.filters | 45 ++++++ .../Utils/TextTransform.h | 2 +- .../Views/RawTextViewManager.cpp | 57 +------- .../Views/RawTextViewManager.h | 3 - .../Views/Text/TextHighlighterVisitor.cpp | 52 +++++++ .../Views/Text/TextHighlighterVisitor.h | 37 +++++ .../Views/Text/TextParentVisitor.cpp | 12 ++ .../Views/Text/TextParentVisitor.h | 19 +++ .../Text/TextPropertyChangedParentVisitor.cpp | 69 +++++++++ .../Text/TextPropertyChangedParentVisitor.h | 42 ++++++ .../Views/Text/TextTransformParentVisitor.cpp | 21 +++ .../Views/Text/TextTransformParentVisitor.h | 23 +++ .../Views/Text/TextTransformVisitor.cpp | 70 +++++++++ .../Views/Text/TextTransformVisitor.h | 34 +++++ .../Views/Text/TextVisitor.cpp | 56 ++++++++ .../Views/Text/TextVisitor.h | 34 +++++ .../Views/Text/TextVisitorScope.h | 35 +++++ .../Views/Text/TextVisitors.h | 40 ++++++ .../Views/TextViewManager.cpp | 135 ++++-------------- .../Views/TextViewManager.h | 14 +- .../Views/VirtualTextViewManager.cpp | 104 ++------------ .../Views/VirtualTextViewManager.h | 11 +- 24 files changed, 658 insertions(+), 278 deletions(-) create mode 100644 change/react-native-windows-f7be41bd-666d-4885-aa40-4c801dad1528.json create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.cpp create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.h create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.cpp create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.h create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextPropertyChangedParentVisitor.cpp create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextPropertyChangedParentVisitor.h create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextTransformParentVisitor.cpp create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextTransformParentVisitor.h create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextTransformVisitor.cpp create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextTransformVisitor.h create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextVisitor.cpp create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextVisitor.h create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextVisitorScope.h create mode 100644 vnext/Microsoft.ReactNative/Views/Text/TextVisitors.h diff --git a/change/react-native-windows-f7be41bd-666d-4885-aa40-4c801dad1528.json b/change/react-native-windows-f7be41bd-666d-4885-aa40-4c801dad1528.json new file mode 100644 index 00000000000..62febdbd585 --- /dev/null +++ b/change/react-native-windows-f7be41bd-666d-4885-aa40-4c801dad1528.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Initial implementation of TextVisitor", + "packageName": "react-native-windows", + "email": "erozell@outlook.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj index aad0e3b7473..fb64a8a37f7 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj @@ -344,6 +344,14 @@ + + + + + + + + @@ -646,6 +654,12 @@ + + + + + + diff --git a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters index 96cd6553a83..ca56575dc1d 100644 --- a/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters +++ b/vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters @@ -309,6 +309,24 @@ ReactHost + + Views\Text + + + Views\Text + + + Views\Text + + + Views\Text + + + Views\Text + + + Views\Text + @@ -672,6 +690,30 @@ Utils + + Views\Text + + + Views\Text + + + Views\Text + + + Views\Text + + + Views\Text + + + Views\Text + + + Views\Text + + + Views\Text + @@ -755,6 +797,9 @@ {24290499-e864-407e-89c0-473f2b8dbb6e} + + {8b871c42-6131-44c2-a73f-f1061780e97f} + diff --git a/vnext/Microsoft.ReactNative/Utils/TextTransform.h b/vnext/Microsoft.ReactNative/Utils/TextTransform.h index de9c554b4e3..ebe09c55be3 100644 --- a/vnext/Microsoft.ReactNative/Utils/TextTransform.h +++ b/vnext/Microsoft.ReactNative/Utils/TextTransform.h @@ -4,5 +4,5 @@ #pragma once namespace Microsoft::ReactNative { -enum class TextTransform : uint8_t { Undefined, None, Uppercase, Lowercase, Capitalize }; +enum class TextTransform : uint8_t { Undefined = 0, None, Uppercase, Lowercase, Capitalize }; } diff --git a/vnext/Microsoft.ReactNative/Views/RawTextViewManager.cpp b/vnext/Microsoft.ReactNative/Views/RawTextViewManager.cpp index 468c593cdb6..790e94a9048 100644 --- a/vnext/Microsoft.ReactNative/Views/RawTextViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/RawTextViewManager.cpp @@ -4,19 +4,12 @@ #include "pch.h" #include "RawTextViewManager.h" -#include "TextViewManager.h" -#include "VirtualTextViewManager.h" #include +#include -#include -#include -#include - -#include -#include -#include #include +#include #include namespace winrt { @@ -52,56 +45,14 @@ bool RawTextViewManager::UpdateProperty( if (propertyName == "text") { run.Text(asHstring(propertyValue)); static_cast(nodeToUpdate)->originalText = winrt::hstring{}; - NotifyAncestorsTextChanged(nodeToUpdate); + ApplyTextTransformToChild(nodeToUpdate); + NotifyAncestorsTextPropertyChanged(nodeToUpdate, PropertyChangeType::Text); } else { return Super::UpdateProperty(nodeToUpdate, propertyName, propertyValue); } return true; } -void RawTextViewManager::NotifyAncestorsTextChanged(ShadowNodeBase *nodeToUpdate) { - if (auto uiManager = GetNativeUIManager(GetReactContext()).lock()) { - auto host = uiManager->getHost(); - ShadowNodeBase *parent = static_cast(host->FindShadowNodeForTag(nodeToUpdate->GetParent())); - TextTransform textTransform = TextTransform::Undefined; - auto isNested = false; - while (parent) { - auto viewManager = parent->GetViewManager(); - const auto nodeType = viewManager->GetName(); - if (IsTextShadowNode(parent)) { - const auto textViewManager = static_cast(viewManager); - if (textTransform == TextTransform::Undefined) { - textTransform = textViewManager->GetTextTransformValue(parent); - } - - VirtualTextShadowNode::ApplyTextTransform( - *nodeToUpdate, textTransform, /* forceUpdate = */ false, /* isRoot = */ false); - - if (!isNested && parent->m_children.size() == 1) { - auto view = parent->GetView(); - auto textBlock = view.try_as(); - if (textBlock != nullptr) { - const auto run = nodeToUpdate->GetView().try_as(); - if (run != nullptr) { - textBlock.Text(run.Text()); - } - } - } - - (static_cast(viewManager))->OnDescendantTextPropertyChanged(parent); - - // We have reached the parent TextBlock, so there're no more parent elements in this tree. - break; - } else if (IsVirtualTextShadowNode(parent) && textTransform == TextTransform::Undefined) { - textTransform = static_cast(parent)->textTransform; - } - - parent = static_cast(host->FindShadowNodeForTag(parent->GetParent())); - isNested = true; - } - } -} - void RawTextViewManager::SetLayoutProps( ShadowNodeBase & /*nodeToUpdate*/, const XamlView & /*viewToUpdate*/, diff --git a/vnext/Microsoft.ReactNative/Views/RawTextViewManager.h b/vnext/Microsoft.ReactNative/Views/RawTextViewManager.h index 3d13de2cb4b..8f9e3688607 100644 --- a/vnext/Microsoft.ReactNative/Views/RawTextViewManager.h +++ b/vnext/Microsoft.ReactNative/Views/RawTextViewManager.h @@ -41,9 +41,6 @@ class RawTextViewManager : public ViewManagerBase { const winrt::Microsoft::ReactNative::JSValue &propertyValue) override; XamlView CreateViewCore(int64_t tag, const winrt::Microsoft::ReactNative::JSValueObject &) override; - - private: - void NotifyAncestorsTextChanged(ShadowNodeBase *nodeToUpdate); }; } // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.cpp b/vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.cpp new file mode 100644 index 00000000000..a92ec7de34c --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.cpp @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TextHighlighterVisitor.h" +#include +#include +#include +#include +#include "TextVisitorScope.h" + +namespace winrt { +using namespace xaml::Documents; +} // namespace winrt + +namespace Microsoft::ReactNative { + +void TextHighlighterVisitor::VisitRawText(ShadowNodeBase *node) { + const auto textNode = static_cast(node); + m_startIndex += textNode->GetView().as().Text().size(); +} + +void TextHighlighterVisitor::VisitVirtualText(ShadowNodeBase *node) { + const auto textNode = static_cast(node); + const auto foregroundColor = textNode->foregroundColor; + const auto backgroundColor = textNode->backgroundColor; + const auto needsHighlighter = RequiresTextHighlighter(foregroundColor, backgroundColor); + TextVisitorScope foregroundScope{m_foregroundColors, foregroundColor}; + TextVisitorScope backgroundScope{m_backgroundColors, backgroundColor}; + + const auto startIndex = m_startIndex; + const auto initialHighlighterCount = highlighters.size(); + Super::VisitVirtualText(node); + + if (needsHighlighter) { + winrt::TextHighlighter highlighter; + highlighter.Background(SolidBrushFromColor(m_backgroundColors.top().value())); + const auto inheritedForegroundColor = m_foregroundColors.top(); + if (inheritedForegroundColor.has_value()) { + highlighter.Foreground(SolidBrushFromColor(inheritedForegroundColor.value())); + } + highlighter.Ranges().Append({startIndex, m_startIndex - startIndex}); + highlighters.push_back(highlighter); + } else if (highlighters.size() == initialHighlighterCount) { + textNode->hasDescendantTextHighlighter = false; + } +} + +bool TextHighlighterVisitor::RequiresTextHighlighter(Color foregroundColor, Color backgroundColor) { + return backgroundColor || m_backgroundColors.top() && foregroundColor; +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.h b/vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.h new file mode 100644 index 00000000000..ed83c5b9e6a --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include "TextVisitor.h" + +namespace Microsoft::ReactNative { + +class TextHighlighterVisitor : public TextVisitor { + using Super = TextVisitor; + using Color = std::optional; + + public: + TextHighlighterVisitor(Color foregroundColor, Color backgroundColor) : Super() { + m_foregroundColors.push(foregroundColor); + m_backgroundColors.push(backgroundColor); + } + + std::vector highlighters{}; + + protected: + void VisitRawText(ShadowNodeBase *node) override; + + void VisitVirtualText(ShadowNodeBase *node) override; + + private: + int m_startIndex{0}; + std::stack m_foregroundColors; + std::stack m_backgroundColors; + + bool RequiresTextHighlighter(Color foregroundColor, Color backgroundColor); +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.cpp b/vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.cpp new file mode 100644 index 00000000000..75abeac544b --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.cpp @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TextParentVisitor.h" + +namespace Microsoft::ReactNative { + +void TextParentVisitor::VisitCore(ShadowNodeBase *node) { + Visit(GetShadowNode(node->m_parent)); +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.h b/vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.h new file mode 100644 index 00000000000..ddf1048d17a --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "TextVisitor.h" + +namespace Microsoft::ReactNative { + +class TextParentVisitor : public TextVisitor { + using Super = TextVisitor; + + protected: + void VisitCore(ShadowNodeBase *node) override; + + void VisitText(ShadowNodeBase *node) override{}; +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextPropertyChangedParentVisitor.cpp b/vnext/Microsoft.ReactNative/Views/Text/TextPropertyChangedParentVisitor.cpp new file mode 100644 index 00000000000..c7de7fbfe09 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextPropertyChangedParentVisitor.cpp @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TextPropertyChangedParentVisitor.h" +#include +#include +#include +#include +#include +#include +#include + +namespace winrt { +using namespace xaml::Automation; +using namespace xaml::Automation::Peers; +using namespace xaml::Documents; +} // namespace winrt + +namespace Microsoft::ReactNative { +void TextPropertyChangedParentVisitor::VisitCore(ShadowNodeBase *node) { + // Update nested flag fast text updates + m_isNested = !IsRawTextShadowNode(node); + Super::VisitCore(node); +} + +void TextPropertyChangedParentVisitor::VisitText(ShadowNodeBase *node) { + // Raise LiveRegionChanged event + const auto isTextUpdate = HasPropertyChangeType(PropertyChangeType::Text); + if (isTextUpdate) { + const auto element = node->GetView().as(); + + // If name is set, it's controlled by accessibilityLabel, and it's already + // handled in FrameworkElementViewManager. Here it only handles when name is + // not set. + if (xaml::Automation::AutomationProperties::GetLiveSetting(element) != winrt::AutomationLiveSetting::Off && + xaml::Automation::AutomationProperties::GetName(element).empty() && + xaml::Automation::AutomationProperties::GetAccessibilityView(element) != winrt::AccessibilityView::Raw) { + if (auto peer = xaml::Automation::Peers::FrameworkElementAutomationPeer::FromElement(element)) { + peer.RaiseAutomationEvent(winrt::AutomationEvents::LiveRegionChanged); + } + } + + // Update fast text content + if (!m_isNested && node->m_children.size() == 1) { + if (const auto childNode = GetShadowNode(node->m_children[0])) { + const auto run = static_cast(childNode)->GetView().as(); + element.Text(run.Text()); + } + } + } + + // Refresh text highlighters + const auto isHighlightAdded = HasPropertyChangeType(PropertyChangeType::AddHighlight); + const auto isHighlightRemoved = HasPropertyChangeType(PropertyChangeType::RemoveHighlight); + if (isTextUpdate || isHighlightAdded || isHighlightRemoved) { + TextViewManager::UpdateTextHighlighters(node, isHighlightAdded); + } +} + +void TextPropertyChangedParentVisitor::VisitVirtualText(ShadowNodeBase *node) { + // Update descendant text highlight flag + if (HasPropertyChangeType(PropertyChangeType::AddHighlight)) { + static_cast(node)->hasDescendantTextHighlighter = true; + } + + Super::VisitVirtualText(node); +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextPropertyChangedParentVisitor.h b/vnext/Microsoft.ReactNative/Views/Text/TextPropertyChangedParentVisitor.h new file mode 100644 index 00000000000..202943f8565 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextPropertyChangedParentVisitor.h @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include "TextParentVisitor.h" + +namespace Microsoft::ReactNative { + +enum class PropertyChangeType : std::uint_fast8_t { + None = 0, + Text = 1 << 0, + AddHighlight = 1 << 1, + RemoveHighlight = 1 << 2, +}; + +DEFINE_ENUM_FLAG_OPERATORS(PropertyChangeType); + +class TextPropertyChangedParentVisitor : public TextParentVisitor { + using Super = TextParentVisitor; + + public: + TextPropertyChangedParentVisitor(PropertyChangeType type) : m_propertyChangeType{type} {} + + protected: + void VisitCore(ShadowNodeBase *node) override; + + void VisitText(ShadowNodeBase *node) override; + + void VisitVirtualText(ShadowNodeBase *node) override; + + private: + PropertyChangeType m_propertyChangeType; + bool m_isNested{false}; + + bool HasPropertyChangeType(PropertyChangeType type) { + return (m_propertyChangeType & type) == type; + } +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextTransformParentVisitor.cpp b/vnext/Microsoft.ReactNative/Views/Text/TextTransformParentVisitor.cpp new file mode 100644 index 00000000000..ec9cbd54f15 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextTransformParentVisitor.cpp @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TextTransformParentVisitor.h" +#include +#include + +namespace Microsoft::ReactNative { + +void TextTransformParentVisitor::VisitText(ShadowNodeBase *node) { + textTransform = TextViewManager::GetTextTransformValue(node); +} + +void TextTransformParentVisitor::VisitVirtualText(ShadowNodeBase *node) { + textTransform = static_cast(node)->textTransform; + if (textTransform == TextTransform::Undefined) { + Super::VisitVirtualText(node); + } +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextTransformParentVisitor.h b/vnext/Microsoft.ReactNative/Views/Text/TextTransformParentVisitor.h new file mode 100644 index 00000000000..24439ee2a97 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextTransformParentVisitor.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include "TextParentVisitor.h" + +namespace Microsoft::ReactNative { + +class TextTransformParentVisitor : public TextParentVisitor { + using Super = TextParentVisitor; + + public: + TextTransform textTransform; + + protected: + void VisitText(ShadowNodeBase *node) override; + + void VisitVirtualText(ShadowNodeBase *node) override; +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextTransformVisitor.cpp b/vnext/Microsoft.ReactNative/Views/Text/TextTransformVisitor.cpp new file mode 100644 index 00000000000..84f6c0899fa --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextTransformVisitor.cpp @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TextTransformVisitor.h" +#include +#include +#include +#include +#include +#include "TextVisitorScope.h" + +namespace winrt { +using namespace xaml::Documents; +} // namespace winrt + +namespace Microsoft::ReactNative { + +void TextTransformVisitor::VisitRawText(ShadowNodeBase *node) { + const auto rawTextNode = static_cast(node); + auto originalText = rawTextNode->originalText; + auto run = node->GetView().as(); + // Set originalText on the raw text node if it hasn't been set yet + if (originalText.size() == 0) { + // Lazily setting original text to avoid keeping two copies of all raw text strings + originalText = run.Text(); + rawTextNode->originalText = originalText; + } + + run.Text(TransformableText::TransformText(originalText, m_textTransforms.top())); + + if (std::wcscmp(originalText.c_str(), run.Text().c_str()) == 0) { + // If the transformed text is the same as the original, we no longer need a second copy + rawTextNode->originalText = winrt::hstring{}; + } +} + +void TextTransformVisitor::VisitText(ShadowNodeBase *node) { + const auto transform = TextViewManager::GetTextTransformValue(node); + if (ShouldApplyTransform(transform)) { + TextVisitorScope scope{m_textTransforms, transform}; + Super::VisitText(node); + } +} + +void TextTransformVisitor::VisitVirtualText(ShadowNodeBase *node) { + const auto textNode = static_cast(node); + const auto transform = textNode->textTransform; + if (ShouldApplyTransform(transform)) { + TextVisitorScope scope{m_textTransforms, transform}; + Super::VisitVirtualText(node); + } +} + +bool TextTransformVisitor::ShouldApplyTransform(TextTransform transform) { + // If the visitor is applied in the context of a textTransfrom prop value + // change, the m_forceUpdate flag is set to true. + if (m_forceUpdate) { + // When the textTransform prop changes, only recurse if at the root node + // or if the current node is has an undefined textTransform. + return m_textTransforms.size() == 1 || transform == TextTransform::Undefined; + } else { + // When a node is added to the tree, only recurse if the added node is has + // an undefined textTransform and the parent is not "none" or undefined. + const auto parentTransform = m_textTransforms.top(); + return parentTransform != TextTransform::Undefined && parentTransform != TextTransform::None && + transform == TextTransform::Undefined; + } +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextTransformVisitor.h b/vnext/Microsoft.ReactNative/Views/Text/TextTransformVisitor.h new file mode 100644 index 00000000000..85a14afbb46 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextTransformVisitor.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include "TextVisitor.h" + +namespace Microsoft::ReactNative { + +class TextTransformVisitor : public TextVisitor { + using Super = TextVisitor; + + public: + TextTransformVisitor(TextTransform parentTransform, bool forceUpdate) : Super(), m_forceUpdate{forceUpdate} { + m_textTransforms.push(parentTransform); + } + + protected: + void VisitRawText(ShadowNodeBase *node) override; + + void VisitText(ShadowNodeBase *node) override; + + void VisitVirtualText(ShadowNodeBase *node) override; + + private: + std::stack m_textTransforms; + bool m_forceUpdate; + + bool ShouldApplyTransform(TextTransform transform); +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextVisitor.cpp b/vnext/Microsoft.ReactNative/Views/Text/TextVisitor.cpp new file mode 100644 index 00000000000..9d9562a4e25 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextVisitor.cpp @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "TextVisitor.h" +#include + +namespace Microsoft::ReactNative { + +void TextVisitor::Visit(ShadowNode *node) { + if (!node || !EnsureNativeUIManager(node)) + return; + + const auto baseNode = static_cast(node); + if (IsTextShadowNode(baseNode)) { + VisitText(baseNode); + } else if (IsVirtualTextShadowNode(baseNode)) { + VisitVirtualText(baseNode); + } else if (IsRawTextShadowNode(baseNode)) { + VisitRawText(baseNode); + } else { + VisitCore(baseNode); + } +} + +void TextVisitor::VisitCore(ShadowNodeBase *node) { + for (auto childTag : node->m_children) { + Visit(GetShadowNode(childTag)); + } +} + +void TextVisitor::VisitRawText(ShadowNodeBase *node) { + VisitCore(node); +} + +void TextVisitor::VisitText(ShadowNodeBase *node) { + VisitCore(node); +} + +void TextVisitor::VisitVirtualText(ShadowNodeBase *node) { + VisitCore(node); +} + +ShadowNode *TextVisitor::GetShadowNode(int64_t tag) { + return m_uiManager->getHost()->FindShadowNodeForTag(tag); +} + +std::shared_ptr TextVisitor::EnsureNativeUIManager(ShadowNode *node) { + if (!m_uiManager) { + const auto baseNode = static_cast(node); + m_uiManager = GetNativeUIManager(baseNode->GetViewManager()->GetReactContext()).lock(); + } + + return m_uiManager; +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextVisitor.h b/vnext/Microsoft.ReactNative/Views/Text/TextVisitor.h new file mode 100644 index 00000000000..64cb97ac371 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextVisitor.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include +#include + +namespace Microsoft::ReactNative { + +class TextVisitor { + public: + void Visit(ShadowNode *node); + + protected: + virtual void VisitCore(ShadowNodeBase *node); + + virtual void VisitRawText(ShadowNodeBase *node); + + virtual void VisitText(ShadowNodeBase *node); + + virtual void VisitVirtualText(ShadowNodeBase *node); + + ShadowNode *GetShadowNode(int64_t tag); + + private: + std::shared_ptr m_uiManager; + + std::shared_ptr EnsureNativeUIManager(ShadowNode *node); +}; + +}; // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextVisitorScope.h b/vnext/Microsoft.ReactNative/Views/Text/TextVisitorScope.h new file mode 100644 index 00000000000..b29440554b0 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextVisitorScope.h @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include + +namespace Microsoft::ReactNative { + +template +struct TextVisitorScope { + TextVisitorScope(std::stack &stack, T scopedValue) : m_stack{stack} { + // There's an assumption here that the scoped value should not be pushed if + // it has a default value. Check this assumption for future usages. + if (scopedValue != T{}) { + m_stack.push(scopedValue); + m_pushed = true; + } + } + + ~TextVisitorScope() { + if (m_pushed) { + m_stack.pop(); + } + } + + TextVisitorScope(TextVisitorScope const &) = delete; + TextVisitorScope &operator=(TextVisitorScope other) = delete; + + private: + std::stack &m_stack; + bool m_pushed{false}; +}; + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/Text/TextVisitors.h b/vnext/Microsoft.ReactNative/Views/Text/TextVisitors.h new file mode 100644 index 00000000000..268c4682e2b --- /dev/null +++ b/vnext/Microsoft.ReactNative/Views/Text/TextVisitors.h @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "TextHighlighterVisitor.h" +#include "TextPropertyChangedParentVisitor.h" +#include "TextTransformParentVisitor.h" +#include "TextTransformVisitor.h" + +namespace Microsoft::ReactNative { +using Color = std::optional; + +static inline std::vector +GetNestedTextHighlighters(ShadowNode *node, Color foregroundColor, Color backgroundColor) { + TextHighlighterVisitor visitor{foregroundColor, backgroundColor}; + visitor.Visit(node); + return visitor.highlighters; +} + +static inline void ApplyTextTransformToChild(ShadowNode *node) { + TextTransformParentVisitor parentVisitor; + parentVisitor.Visit(node); + TextTransformVisitor visitor{parentVisitor.textTransform, false}; + visitor.Visit(node); +} + +static inline void UpdateTextTransformForChildren(ShadowNode *node) { + TextTransformParentVisitor parentVisitor; + parentVisitor.Visit(node); + TextTransformVisitor visitor{parentVisitor.textTransform, true}; + visitor.Visit(node); +} + +static inline void NotifyAncestorsTextPropertyChanged(ShadowNode *node, PropertyChangeType type) { + TextPropertyChangedParentVisitor visitor{type}; + visitor.Visit(node); +} + +} // namespace Microsoft::ReactNative diff --git a/vnext/Microsoft.ReactNative/Views/TextViewManager.cpp b/vnext/Microsoft.ReactNative/Views/TextViewManager.cpp index e8ae7c581e9..f8ad22a46b6 100644 --- a/vnext/Microsoft.ReactNative/Views/TextViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/TextViewManager.cpp @@ -7,14 +7,10 @@ #include "Utils/ShadowNodeTypeUtils.h" #include "Utils/XamlIslandUtils.h" -#include -#include -#include #include +#include #include -#include -#include #include #include #include @@ -36,11 +32,10 @@ class TextShadowNode final : public ShadowNodeBase { private: ShadowNode *m_firstChildNode; + bool m_hasDescendantTextHighlighter{false}; std::optional m_backgroundColor{}; std::optional m_foregroundColor{}; - int32_t m_prevCursorEnd = 0; - public: TextShadowNode() { m_firstChildNode = nullptr; @@ -51,12 +46,11 @@ class TextShadowNode final : public ShadowNodeBase { void AddView(ShadowNode &child, int64_t index) override { auto &childNode = static_cast(child); - VirtualTextShadowNode::ApplyTextTransform( - childNode, textTransform, /* forceUpdate = */ false, /* isRoot = */ false); + ApplyTextTransformToChild(&child); if (IsVirtualTextShadowNode(&childNode)) { auto &textChildNode = static_cast(childNode); - m_hasDescendantBackgroundColor |= textChildNode.m_hasDescendantBackgroundColor; + m_hasDescendantTextHighlighter |= textChildNode.hasDescendantTextHighlighter; } auto addInline = true; @@ -66,7 +60,6 @@ class TextShadowNode final : public ShadowNodeBase { m_firstChildNode = &child; auto textBlock = this->GetView().as(); textBlock.Text(run.Text()); - m_prevCursorEnd += textBlock.Text().size(); addInline = false; } } else if (index == 1 && m_firstChildNode != nullptr) { @@ -101,92 +94,36 @@ class TextShadowNode final : public ShadowNodeBase { const auto textBlock = this->GetView().as(); textBlock.TextHighlighters().Clear(); - auto nestedIndex = 0; - if (m_hasDescendantBackgroundColor) { - const auto highlighterCount = textBlock.TextHighlighters().Size(); - if (const auto uiManager = GetNativeUIManager(GetViewManager()->GetReactContext()).lock()) { - for (auto childTag : m_children) { - if (const auto childNode = uiManager->getHost()->FindShadowNodeForTag(childTag)) { - nestedIndex = AddNestedTextHighlighter( - m_backgroundColor, m_foregroundColor, static_cast(childNode), nestedIndex); - } + // Since TextShadowNode is not public, we lift some of the recursive + // algorithm into the shadow node implementation to detect when no + // descendants have background colors and we can skip recursion. + if (m_hasDescendantTextHighlighter) { + const auto highlighters = GetNestedTextHighlighters(this, m_foregroundColor, m_backgroundColor); + if (highlighters.size() == 0) { + m_hasDescendantTextHighlighter = false; + } else { + // We must add the highlighters in reverse order, as highlighters + // "deeper" in the text tree should render at the top. + auto iter = highlighters.rbegin(); + while (iter != highlighters.rend()) { + textBlock.TextHighlighters().Append(*iter); + ++iter; } } - - if (textBlock.TextHighlighters().Size() == 0) { - m_hasDescendantBackgroundColor = false; - } - } else { - nestedIndex = textBlock.Text().size(); } if (m_backgroundColor) { winrt::TextHighlighter highlighter{}; - highlighter.Ranges().Append({0, nestedIndex}); + highlighter.Ranges().Append({0, static_cast(textBlock.Text().size())}); highlighter.Background(SolidBrushFromColor(m_backgroundColor.value())); if (m_foregroundColor) { highlighter.Foreground(SolidBrushFromColor(m_foregroundColor.value())); } - GetView().as().TextHighlighters().InsertAt(0, highlighter); + textBlock.TextHighlighters().InsertAt(0, highlighter); } } - int AddNestedTextHighlighter( - const std::optional &backgroundColor, - const std::optional &foregroundColor, - ShadowNodeBase *node, - int startIndex) { - if (const auto run = node->GetView().try_as()) { - return startIndex + run.Text().size(); - } else if (const auto span = node->GetView().try_as()) { - const auto textBlock = GetView().as(); - winrt::TextHighlighter highlighter{nullptr}; - auto parentBackgroundColor = backgroundColor; - auto parentForegroundColor = foregroundColor; - if (IsVirtualTextShadowNode(node)) { - const auto virtualTextNode = static_cast(node); - const auto requiresHighlighter = - virtualTextNode->m_backgroundColor || (backgroundColor && virtualTextNode->m_foregroundColor); - if (requiresHighlighter) { - highlighter = {}; - parentBackgroundColor = - virtualTextNode->m_backgroundColor ? virtualTextNode->m_backgroundColor : parentBackgroundColor; - parentForegroundColor = - virtualTextNode->m_foregroundColor ? virtualTextNode->m_foregroundColor : parentForegroundColor; - highlighter.Background(SolidBrushFromColor(parentBackgroundColor.value())); - if (parentForegroundColor) { - highlighter.Foreground(SolidBrushFromColor(parentForegroundColor.value())); - } - } - } - - const auto initialHighlighterCount = textBlock.TextHighlighters().Size(); - auto nestedIndex = startIndex; - if (const auto uiManager = GetNativeUIManager(node->GetViewManager()->GetReactContext()).lock()) { - for (auto childTag : node->m_children) { - if (const auto childNode = uiManager->getHost()->FindShadowNodeForTag(childTag)) { - nestedIndex = AddNestedTextHighlighter( - parentBackgroundColor, parentForegroundColor, static_cast(childNode), nestedIndex); - } - } - } - - if (highlighter) { - highlighter.Ranges().Append({startIndex, nestedIndex - startIndex}); - textBlock.TextHighlighters().InsertAt(0, highlighter); - } else if (IsVirtualTextShadowNode(node) && textBlock.TextHighlighters().Size() == initialHighlighterCount) { - const auto virtualTextNode = static_cast(node); - virtualTextNode->m_hasDescendantBackgroundColor = false; - } - - return nestedIndex; - } - - return 0; - } - TextTransform textTransform{TextTransform::Undefined}; - bool m_hasDescendantBackgroundColor{false}; }; TextViewManager::TextViewManager(const Mso::React::IReactContext &context) : Super(context) {} @@ -223,8 +160,7 @@ bool TextViewManager::UpdateProperty( } else if (propertyName == "textTransform") { auto textNode = static_cast(nodeToUpdate); textNode->textTransform = TransformableText::GetTextTransform(propertyValue); - VirtualTextShadowNode::ApplyTextTransform( - *textNode, textNode->textTransform, /* forceUpdate = */ true, /* isRoot = */ true); + UpdateTextTransformForChildren(nodeToUpdate); } else if (TryUpdatePadding(nodeToUpdate, textBlock, propertyName, propertyValue)) { } else if (TryUpdateTextAlignment(textBlock, propertyName, propertyValue)) { } else if (TryUpdateTextTrimming(textBlock, propertyName, propertyValue)) { @@ -342,38 +278,17 @@ YGMeasureFunc TextViewManager::GetYogaCustomMeasureFunc() const { return DefaultYogaSelfMeasureFunc; } -void TextViewManager::OnDescendantTextPropertyChanged(ShadowNodeBase *node, PropertyChangeType propertyChangeType) { +/*static*/ void TextViewManager::UpdateTextHighlighters(ShadowNodeBase *node, bool highlightAdded) { if (IsTextShadowNode(node)) { const auto textNode = static_cast(node); - - if ((propertyChangeType & PropertyChangeType::Text) == PropertyChangeType::Text) { - const auto element = node->GetView().as(); - - // If name is set, it's controlled by accessibilityLabel, and it's already - // handled in FrameworkElementViewManager. Here it only handles when name is - // not set. - if (xaml::Automation::AutomationProperties::GetLiveSetting(element) != winrt::AutomationLiveSetting::Off && - xaml::Automation::AutomationProperties::GetName(element).empty() && - xaml::Automation::AutomationProperties::GetAccessibilityView(element) != - winrt::Peers::AccessibilityView::Raw) { - if (auto peer = xaml::Automation::Peers::FrameworkElementAutomationPeer::FromElement(element)) { - peer.RaiseAutomationEvent(winrt::AutomationEvents::LiveRegionChanged); - } - } + if (highlightAdded) { + textNode->m_hasDescendantTextHighlighter = true; } - - // If a property change added a background color to the text tree, update - // the flag to signal recursive highlighter updates are required. - if ((propertyChangeType & PropertyChangeType::AddBackgroundColor) == PropertyChangeType::AddBackgroundColor) { - textNode->m_hasDescendantBackgroundColor = true; - } - - // Recalculate text highlighters textNode->RecalculateTextHighlighters(); } } -TextTransform TextViewManager::GetTextTransformValue(ShadowNodeBase *node) { +/*static*/ TextTransform TextViewManager::GetTextTransformValue(ShadowNodeBase *node) { if (IsTextShadowNode(node)) { return static_cast(node)->textTransform; } diff --git a/vnext/Microsoft.ReactNative/Views/TextViewManager.h b/vnext/Microsoft.ReactNative/Views/TextViewManager.h index 08e4a0df89e..abe7ca175e4 100644 --- a/vnext/Microsoft.ReactNative/Views/TextViewManager.h +++ b/vnext/Microsoft.ReactNative/Views/TextViewManager.h @@ -8,14 +8,6 @@ namespace Microsoft::ReactNative { -enum class PropertyChangeType : std::uint_fast8_t { - None = 0, - Text = 1 << 0, - AddBackgroundColor = 1 << 1, -}; - -DEFINE_ENUM_FLAG_OPERATORS(PropertyChangeType); - class TextViewManager : public FrameworkElementViewManager { using Super = FrameworkElementViewManager; @@ -32,11 +24,9 @@ class TextViewManager : public FrameworkElementViewManager { YGMeasureFunc GetYogaCustomMeasureFunc() const override; - void OnDescendantTextPropertyChanged( - ShadowNodeBase *node, - PropertyChangeType propertyChangeType = PropertyChangeType::Text); + static void UpdateTextHighlighters(ShadowNodeBase *node, bool highlightAdded); - TextTransform GetTextTransformValue(ShadowNodeBase *node); + static TextTransform GetTextTransformValue(ShadowNodeBase *node); protected: bool UpdateProperty( diff --git a/vnext/Microsoft.ReactNative/Views/VirtualTextViewManager.cpp b/vnext/Microsoft.ReactNative/Views/VirtualTextViewManager.cpp index 6c390a4ce58..fffc12d1e14 100644 --- a/vnext/Microsoft.ReactNative/Views/VirtualTextViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/VirtualTextViewManager.cpp @@ -5,16 +5,12 @@ #include "VirtualTextViewManager.h" -#include - -#include -#include -#include #include #include #include #include #include +#include namespace winrt { using namespace Windows::UI; @@ -27,96 +23,25 @@ namespace Microsoft::ReactNative { void VirtualTextShadowNode::AddView(ShadowNode &child, int64_t index) { auto &childNode = static_cast(child); - ApplyTextTransform(childNode, textTransform, /* forceUpdate = */ false, /* isRoot = */ false); + ApplyTextTransformToChild(&child); auto propertyChangeType = PropertyChangeType::Text; if (IsVirtualTextShadowNode(&childNode)) { const auto &childTextNode = static_cast(childNode); - m_hasDescendantBackgroundColor |= childTextNode.m_hasDescendantBackgroundColor; propertyChangeType |= - childTextNode.m_backgroundColor ? PropertyChangeType::AddBackgroundColor : PropertyChangeType::None; + childTextNode.hasDescendantTextHighlighter ? PropertyChangeType::AddHighlight : PropertyChangeType::None; } Super::AddView(child, index); - NotifyAncestorsTextPropertyChanged(propertyChangeType); + NotifyAncestorsTextPropertyChanged(this, propertyChangeType); } void VirtualTextShadowNode::RemoveChildAt(int64_t indexToRemove) { Super::RemoveChildAt(indexToRemove); - NotifyAncestorsTextPropertyChanged(PropertyChangeType::Text); + NotifyAncestorsTextPropertyChanged(this, PropertyChangeType::Text); } void VirtualTextShadowNode::removeAllChildren() { Super::removeAllChildren(); - NotifyAncestorsTextPropertyChanged(PropertyChangeType::Text); -} - -void VirtualTextShadowNode::ApplyTextTransform( - ShadowNodeBase &node, - TextTransform transform, - bool forceUpdate, - bool isRoot) { - // The `forceUpdate` option is used to force the tree to update, even if the - // transform value is undefined or set to 'none'. This is used when a leaf - // raw text value has changed, or a textTransform prop has changed. - if (forceUpdate || (transform != TextTransform::Undefined && transform != TextTransform::None)) { - // Use the view manager name to determine the node type - const auto viewManager = node.GetViewManager(); - const auto nodeType = viewManager->GetName(); - - // Base case: apply the inherited textTransform to the raw text node - if (IsRawTextShadowNode(&node)) { - auto &rawTextNode = static_cast(node); - auto originalText = rawTextNode.originalText; - auto run = node.GetView().try_as(); - // Set originalText on the raw text node if it hasn't been set yet - if (originalText.size() == 0) { - // Lazily setting original text to avoid keeping two copies of all raw text strings - originalText = run.Text(); - rawTextNode.originalText = originalText; - } - - run.Text(TransformableText::TransformText(originalText, transform)); - - if (std::wcscmp(originalText.c_str(), run.Text().c_str()) == 0) { - // If the transformed text is the same as the original, we no longer need a second copy - rawTextNode.originalText = winrt::hstring{}; - } - } else { - // Recursively apply the textTransform to the children of the composite text node - if (IsVirtualTextShadowNode(&node)) { - auto &virtualTextNode = static_cast(node); - // If this is not the root call, we can skip sub-trees with explicit textTransform settings. - if (!isRoot && virtualTextNode.textTransform != TextTransform::Undefined) { - return; - } - } - - if (auto uiManager = GetNativeUIManager(viewManager->GetReactContext()).lock()) { - for (auto childTag : node.m_children) { - const auto childNode = static_cast(uiManager->getHost()->FindShadowNodeForTag(childTag)); - ApplyTextTransform(*childNode, transform, forceUpdate, /* isRoot = */ false); - } - } - } - } -} - -void VirtualTextShadowNode::NotifyAncestorsTextPropertyChanged(PropertyChangeType propertyChangeType) { - if (auto uiManager = GetNativeUIManager(GetViewManager()->GetReactContext()).lock()) { - auto host = uiManager->getHost(); - ShadowNodeBase *parent = static_cast(host->FindShadowNodeForTag(m_parent)); - while (parent) { - auto viewManager = parent->GetViewManager(); - const auto nodeType = viewManager->GetName(); - if (IsTextShadowNode(parent)) { - (static_cast(viewManager))->OnDescendantTextPropertyChanged(parent, propertyChangeType); - break; - } else if (IsVirtualTextShadowNode(parent)) { - auto textParent = static_cast(parent); - textParent->m_hasDescendantBackgroundColor |= m_hasDescendantBackgroundColor; - } - parent = static_cast(host->FindShadowNodeForTag(parent->GetParent())); - } - } + NotifyAncestorsTextPropertyChanged(this, PropertyChangeType::Text); } VirtualTextViewManager::VirtualTextViewManager(const Mso::React::IReactContext &context) : Super(context) {} @@ -142,11 +67,10 @@ bool VirtualTextViewManager::UpdateProperty( if (TryUpdateForeground(span, propertyName, propertyValue)) { auto node = static_cast(nodeToUpdate); if (IsValidOptionalColorValue(propertyValue)) { - node->m_foregroundColor = OptionalColorFrom(propertyValue); - node->m_hasDescendantBackgroundColor |= node->m_foregroundColor.has_value(); + node->foregroundColor = OptionalColorFrom(propertyValue); const auto propertyChangeType = - node->m_foregroundColor ? PropertyChangeType::AddBackgroundColor : PropertyChangeType::None; - node->NotifyAncestorsTextPropertyChanged(propertyChangeType); + node->foregroundColor ? PropertyChangeType::AddHighlight : PropertyChangeType::RemoveHighlight; + NotifyAncestorsTextPropertyChanged(node, propertyChangeType); } } else if (TryUpdateFontProperties(span, propertyName, propertyValue)) { } else if (TryUpdateCharacterSpacing(span, propertyName, propertyValue)) { @@ -154,16 +78,14 @@ bool VirtualTextViewManager::UpdateProperty( } else if (propertyName == "textTransform") { auto node = static_cast(nodeToUpdate); node->textTransform = TransformableText::GetTextTransform(propertyValue); - VirtualTextShadowNode::ApplyTextTransform( - *node, node->textTransform, /* forceUpdate = */ true, /* isRoot = */ true); + UpdateTextTransformForChildren(nodeToUpdate); } else if (propertyName == "backgroundColor") { auto node = static_cast(nodeToUpdate); if (IsValidOptionalColorValue(propertyValue)) { - node->m_backgroundColor = OptionalColorFrom(propertyValue); - node->m_hasDescendantBackgroundColor |= node->m_backgroundColor.has_value(); + node->backgroundColor = OptionalColorFrom(propertyValue); const auto propertyChangeType = - node->m_backgroundColor ? PropertyChangeType::AddBackgroundColor : PropertyChangeType::None; - node->NotifyAncestorsTextPropertyChanged(propertyChangeType); + node->backgroundColor ? PropertyChangeType::AddHighlight : PropertyChangeType::RemoveHighlight; + NotifyAncestorsTextPropertyChanged(node, propertyChangeType); } } else { return Super::UpdateProperty(nodeToUpdate, propertyName, propertyValue); diff --git a/vnext/Microsoft.ReactNative/Views/VirtualTextViewManager.h b/vnext/Microsoft.ReactNative/Views/VirtualTextViewManager.h index 028ef45f561..e0956b08880 100644 --- a/vnext/Microsoft.ReactNative/Views/VirtualTextViewManager.h +++ b/vnext/Microsoft.ReactNative/Views/VirtualTextViewManager.h @@ -15,18 +15,13 @@ namespace Microsoft::ReactNative { struct VirtualTextShadowNode final : public ShadowNodeBase { using Super = ShadowNodeBase; TextTransform textTransform{TextTransform::Undefined}; + bool hasDescendantTextHighlighter{false}; + std::optional backgroundColor; + std::optional foregroundColor; void AddView(ShadowNode &child, int64_t index) override; void RemoveChildAt(int64_t indexToRemove) override; void removeAllChildren() override; - - void NotifyAncestorsTextPropertyChanged(PropertyChangeType propertyChangeType); - - static void ApplyTextTransform(ShadowNodeBase &node, TextTransform transform, bool forceUpdate, bool isRoot); - - std::optional m_backgroundColor; - std::optional m_foregroundColor; - bool m_hasDescendantBackgroundColor{false}; }; class VirtualTextViewManager : public ViewManagerBase {