Skip to content

Commit

Permalink
Introduces TextVisitor to consolidate text traversal algorithms (micr…
Browse files Browse the repository at this point in the history
…osoft#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 microsoft#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'
  • Loading branch information
rozele authored and chrisglein committed Sep 22, 2021
1 parent 1671258 commit 34356c0
Show file tree
Hide file tree
Showing 24 changed files with 658 additions and 278 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Initial implementation of TextVisitor",
"packageName": "react-native-windows",
"email": "[email protected]",
"dependentChangeType": "patch"
}
14 changes: 14 additions & 0 deletions vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,14 @@
<ClInclude Include="Views\SwitchViewManager.h" />
<ClInclude Include="Views\TextInputViewManager.h" />
<ClInclude Include="Views\TextViewManager.h" />
<ClInclude Include="Views\Text\TextHighlighterVisitor.h" />
<ClInclude Include="Views\Text\TextParentVisitor.h" />
<ClInclude Include="Views\Text\TextPropertyChangedParentVisitor.h" />
<ClInclude Include="Views\Text\TextTransformParentVisitor.h" />
<ClInclude Include="Views\Text\TextTransformVisitor.h" />
<ClInclude Include="Views\Text\TextVisitorScope.h" />
<ClInclude Include="Views\Text\TextVisitor.h" />
<ClInclude Include="Views\Text\TextVisitors.h" />
<ClInclude Include="Views\TouchEventHandler.h" />
<ClInclude Include="Views\ViewControl.h" />
<ClInclude Include="Views\ViewManager.h" />
Expand Down Expand Up @@ -646,6 +654,12 @@
<ClCompile Include="Views\SwitchViewManager.cpp" />
<ClCompile Include="Views\TextInputViewManager.cpp" />
<ClCompile Include="Views\TextViewManager.cpp" />
<ClCompile Include="Views\Text\TextHighlighterVisitor.cpp" />
<ClCompile Include="Views\Text\TextParentVisitor.cpp" />
<ClCompile Include="Views\Text\TextPropertyChangedParentVisitor.cpp" />
<ClCompile Include="Views\Text\TextTransformParentVisitor.cpp" />
<ClCompile Include="Views\Text\TextTransformVisitor.cpp" />
<ClCompile Include="Views\Text\TextVisitor.cpp" />
<ClCompile Include="Views\TouchEventHandler.cpp" />
<ClCompile Include="Views\ViewControl.cpp" />
<ClCompile Include="Views\ViewManagerBase.cpp" />
Expand Down
45 changes: 45 additions & 0 deletions vnext/Microsoft.ReactNative/Microsoft.ReactNative.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,24 @@
<ClCompile Include="ReactHost\JSCallInvokerScheduler.cpp">
<Filter>ReactHost</Filter>
</ClCompile>
<ClCompile Include="Views\Text\TextHighlighterVisitor.cpp">
<Filter>Views\Text</Filter>
</ClCompile>
<ClCompile Include="Views\Text\TextParentVisitor.cpp">
<Filter>Views\Text</Filter>
</ClCompile>
<ClCompile Include="Views\Text\TextPropertyChangedParentVisitor.cpp">
<Filter>Views\Text</Filter>
</ClCompile>
<ClCompile Include="Views\Text\TextTransformParentVisitor.cpp">
<Filter>Views\Text</Filter>
</ClCompile>
<ClCompile Include="Views\Text\TextTransformVisitor.cpp">
<Filter>Views\Text</Filter>
</ClCompile>
<ClCompile Include="Views\Text\TextVisitor.cpp">
<Filter>Views\Text</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="ABICxxModule.h" />
Expand Down Expand Up @@ -672,6 +690,30 @@
<ClInclude Include="Utils\ShadowNodeTypeUtils.h">
<Filter>Utils</Filter>
</ClInclude>
<ClInclude Include="Views\Text\TextHighlighterVisitor.h">
<Filter>Views\Text</Filter>
</ClInclude>
<ClInclude Include="Views\Text\TextParentVisitor.h">
<Filter>Views\Text</Filter>
</ClInclude>
<ClInclude Include="Views\Text\TextPropertyChangedParentVisitor.h">
<Filter>Views\Text</Filter>
</ClInclude>
<ClInclude Include="Views\Text\TextTransformParentVisitor.h">
<Filter>Views\Text</Filter>
</ClInclude>
<ClInclude Include="Views\Text\TextTransformVisitor.h">
<Filter>Views\Text</Filter>
</ClInclude>
<ClInclude Include="Views\Text\TextVisitorScope.h">
<Filter>Views\Text</Filter>
</ClInclude>
<ClInclude Include="Views\Text\TextVisitor.h">
<Filter>Views\Text</Filter>
</ClInclude>
<ClInclude Include="Views\Text\TextVisitors.h">
<Filter>Views\Text</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Midl Include="IJSValueReader.idl" />
Expand Down Expand Up @@ -755,6 +797,9 @@
<Filter Include="JSI">
<UniqueIdentifier>{24290499-e864-407e-89c0-473f2b8dbb6e}</UniqueIdentifier>
</Filter>
<Filter Include="Views\Text">
<UniqueIdentifier>{8b871c42-6131-44c2-a73f-f1061780e97f}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Version.rc" />
Expand Down
2 changes: 1 addition & 1 deletion vnext/Microsoft.ReactNative/Utils/TextTransform.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
57 changes: 4 additions & 53 deletions vnext/Microsoft.ReactNative/Views/RawTextViewManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,12 @@
#include "pch.h"

#include "RawTextViewManager.h"
#include "TextViewManager.h"
#include "VirtualTextViewManager.h"

#include <Views/ShadowNodeBase.h>
#include <Views/Text/TextVisitors.h>

#include <INativeUIManager.h>
#include <Utils/ShadowNodeTypeUtils.h>
#include <Utils/ValueUtils.h>

#include <Modules/NativeUIManager.h>
#include <Modules/PaperUIManagerModule.h>
#include <UI.Xaml.Controls.h>
#include <UI.Xaml.Documents.h>
#include <Utils/ValueUtils.h>
#include <winrt/Windows.Foundation.h>

namespace winrt {
Expand Down Expand Up @@ -52,56 +45,14 @@ bool RawTextViewManager::UpdateProperty(
if (propertyName == "text") {
run.Text(asHstring(propertyValue));
static_cast<RawTextShadowNode *>(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<ShadowNodeBase *>(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<TextViewManager *>(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<winrt::TextBlock>();
if (textBlock != nullptr) {
const auto run = nodeToUpdate->GetView().try_as<winrt::Run>();
if (run != nullptr) {
textBlock.Text(run.Text());
}
}
}

(static_cast<TextViewManager *>(viewManager))->OnDescendantTextPropertyChanged(parent);

// We have reached the parent TextBlock, so there're no more parent <Text> elements in this tree.
break;
} else if (IsVirtualTextShadowNode(parent) && textTransform == TextTransform::Undefined) {
textTransform = static_cast<VirtualTextShadowNode *>(parent)->textTransform;
}

parent = static_cast<ShadowNodeBase *>(host->FindShadowNodeForTag(parent->GetParent()));
isNested = true;
}
}
}

void RawTextViewManager::SetLayoutProps(
ShadowNodeBase & /*nodeToUpdate*/,
const XamlView & /*viewToUpdate*/,
Expand Down
3 changes: 0 additions & 3 deletions vnext/Microsoft.ReactNative/Views/RawTextViewManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
52 changes: 52 additions & 0 deletions vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#include "TextHighlighterVisitor.h"
#include <Utils/ValueUtils.h>
#include <Views/RawTextViewManager.h>
#include <Views/TextViewManager.h>
#include <Views/VirtualTextViewManager.h>
#include "TextVisitorScope.h"

namespace winrt {
using namespace xaml::Documents;
} // namespace winrt

namespace Microsoft::ReactNative {

void TextHighlighterVisitor::VisitRawText(ShadowNodeBase *node) {
const auto textNode = static_cast<RawTextShadowNode *>(node);
m_startIndex += textNode->GetView().as<winrt::Run>().Text().size();
}

void TextHighlighterVisitor::VisitVirtualText(ShadowNodeBase *node) {
const auto textNode = static_cast<VirtualTextShadowNode *>(node);
const auto foregroundColor = textNode->foregroundColor;
const auto backgroundColor = textNode->backgroundColor;
const auto needsHighlighter = RequiresTextHighlighter(foregroundColor, backgroundColor);
TextVisitorScope<Color> foregroundScope{m_foregroundColors, foregroundColor};
TextVisitorScope<Color> 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
37 changes: 37 additions & 0 deletions vnext/Microsoft.ReactNative/Views/Text/TextHighlighterVisitor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#pragma once

#include <Utils/TextTransform.h>
#include <stack>
#include "TextVisitor.h"

namespace Microsoft::ReactNative {

class TextHighlighterVisitor : public TextVisitor {
using Super = TextVisitor;
using Color = std::optional<winrt::Windows::UI::Color>;

public:
TextHighlighterVisitor(Color foregroundColor, Color backgroundColor) : Super() {
m_foregroundColors.push(foregroundColor);
m_backgroundColors.push(backgroundColor);
}

std::vector<xaml::Documents::TextHighlighter> highlighters{};

protected:
void VisitRawText(ShadowNodeBase *node) override;

void VisitVirtualText(ShadowNodeBase *node) override;

private:
int m_startIndex{0};
std::stack<Color> m_foregroundColors;
std::stack<Color> m_backgroundColors;

bool RequiresTextHighlighter(Color foregroundColor, Color backgroundColor);
};

} // namespace Microsoft::ReactNative
12 changes: 12 additions & 0 deletions vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.cpp
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions vnext/Microsoft.ReactNative/Views/Text/TextParentVisitor.h
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#include "TextPropertyChangedParentVisitor.h"
#include <UI.Xaml.Automation.Peers.h>
#include <UI.Xaml.Automation.h>
#include <UI.Xaml.Controls.h>
#include <UI.Xaml.Documents.h>
#include <Utils/ShadowNodeTypeUtils.h>
#include <Views/TextViewManager.h>
#include <Views/VirtualTextViewManager.h>

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<xaml::Controls::TextBlock>();

// 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<ShadowNodeBase *>(childNode)->GetView().as<winrt::Run>();
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<VirtualTextShadowNode *>(node)->hasDescendantTextHighlighter = true;
}

Super::VisitVirtualText(node);
}

} // namespace Microsoft::ReactNative
Loading

0 comments on commit 34356c0

Please sign in to comment.