Skip to content

Commit

Permalink
prototype(FastText): Add option to use TextBlock for simple text (#1256)
Browse files Browse the repository at this point in the history
* prototype(FastText): Add option to use TextBlock for simple text

Simple text, i.e., text with only a single raw text node as a child, can be transformed into a `TextBlock` with a `Text` property, instead of a `RichTextBlock` with a `Paragraph` of `Inlines`. This change detects in the React Native Text node has a single raw text child, and if so, uses the FastText native component. To test if your text nodes are "fast", set `DebugSettings.IsTextPerformanceVisualizationEnabled` to true while debugging your app; it changes optimized text color to green.

Fixes #1252
  • Loading branch information
rozele authored Aug 14, 2018
1 parent e0a98aa commit 7fa57fc
Show file tree
Hide file tree
Showing 11 changed files with 733 additions and 9 deletions.
275 changes: 275 additions & 0 deletions Libraries/Text/Text.windows.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @providesModule Text
* @flow
* @format
*/
'use strict';

const React = require('React');
const ReactNative = require('ReactNative');
const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
const TextPropTypes = require('TextPropTypes');
const Touchable = require('Touchable');
const UIManager = require('UIManager');

const createReactNativeComponentClass = require('createReactNativeComponentClass');
const mergeFast = require('mergeFast');
const processColor = require('processColor');
const {ViewContextTypes} = require('ViewContext');

import type {PressEvent} from 'CoreEventTypes';
import type {TextProps} from 'TextProps';
import type {ViewChildContext} from 'ViewContext';

type State = {
isHighlighted: boolean,
};

type RectOffset = {
top: number,
left: number,
right: number,
bottom: number,
};

const PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};

const viewConfig = {
validAttributes: mergeFast(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
numberOfLines: true,
ellipsizeMode: true,
allowFontScaling: true,
disabled: true,
selectable: true,
selectionColor: true,
adjustsFontSizeToFit: true,
minimumFontScale: true,
textBreakStrategy: true,
}),
uiViewClassName: 'RCTText',
};

const simpleViewConfig = {
validAttributes: mergeFast(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
numberOfLines: true,
ellipsizeMode: true,
allowFontScaling: true,
disabled: true,
selectable: true,
selectionColor: true,
adjustsFontSizeToFit: true,
minimumFontScale: true,
textBreakStrategy: true,
text: true,
}),
uiViewClassName: 'RCTSimpleText',
};

/**
* A React component for displaying text.
*
* See https://facebook.github.io/react-native/docs/text.html
*/
class Text extends ReactNative.NativeComponent<TextProps, State> {
static propTypes = TextPropTypes;
static childContextTypes = ViewContextTypes;
static contextTypes = ViewContextTypes;

static defaultProps = {
accessible: true,
allowFontScaling: true,
ellipsizeMode: 'tail',
};

state = mergeFast(Touchable.Mixin.touchableGetInitialState(), {
isHighlighted: false,
});

viewConfig = viewConfig;

getChildContext(): ViewChildContext {
return {
isInAParentText: true,
};
}

_handlers: ?Object;

_hasPressHandler(): boolean {
return !!this.props.onPress || !!this.props.onLongPress;
}
/**
* These are assigned lazily the first time the responder is set to make plain
* text nodes as cheap as possible.
*/
touchableHandleActivePressIn: ?Function;
touchableHandleActivePressOut: ?Function;
touchableHandlePress: ?Function;
touchableHandleLongPress: ?Function;
touchableHandleResponderGrant: ?Function;
touchableHandleResponderMove: ?Function;
touchableHandleResponderRelease: ?Function;
touchableHandleResponderTerminate: ?Function;
touchableHandleResponderTerminationRequest: ?Function;
touchableGetPressRectOffset: ?Function;

render(): React.Element<any> {
let newProps = this.props;
if (this.props.onStartShouldSetResponder || this._hasPressHandler()) {
if (!this._handlers) {
this._handlers = {
onStartShouldSetResponder: (): boolean => {
const shouldSetFromProps =
this.props.onStartShouldSetResponder &&
this.props.onStartShouldSetResponder();
const setResponder = shouldSetFromProps || this._hasPressHandler();
if (setResponder && !this.touchableHandleActivePressIn) {
// Attach and bind all the other handlers only the first time a touch
// actually happens.
for (const key in Touchable.Mixin) {
if (typeof Touchable.Mixin[key] === 'function') {
(this: any)[key] = Touchable.Mixin[key].bind(this);
}
}
this.touchableHandleActivePressIn = () => {
if (
this.props.suppressHighlighting ||
!this._hasPressHandler()
) {
return;
}
this.setState({
isHighlighted: true,
});
};

this.touchableHandleActivePressOut = () => {
if (
this.props.suppressHighlighting ||
!this._hasPressHandler()
) {
return;
}
this.setState({
isHighlighted: false,
});
};

this.touchableHandlePress = (e: PressEvent) => {
this.props.onPress && this.props.onPress(e);
};

this.touchableHandleLongPress = (e: PressEvent) => {
this.props.onLongPress && this.props.onLongPress(e);
};

this.touchableGetPressRectOffset = function(): RectOffset {
return this.props.pressRetentionOffset || PRESS_RECT_OFFSET;
};
}
return setResponder;
},
onResponderGrant: function(e: SyntheticEvent<>, dispatchID: string) {
// $FlowFixMe TouchableMixin handlers couldn't actually be null
this.touchableHandleResponderGrant(e, dispatchID);
this.props.onResponderGrant &&
this.props.onResponderGrant.apply(this, arguments);
}.bind(this),
onResponderMove: function(e: SyntheticEvent<>) {
// $FlowFixMe TouchableMixin handlers couldn't actually be null
this.touchableHandleResponderMove(e);
this.props.onResponderMove &&
this.props.onResponderMove.apply(this, arguments);
}.bind(this),
onResponderRelease: function(e: SyntheticEvent<>) {
// $FlowFixMe TouchableMixin handlers couldn't actually be null
this.touchableHandleResponderRelease(e);
this.props.onResponderRelease &&
this.props.onResponderRelease.apply(this, arguments);
}.bind(this),
onResponderTerminate: function(e: SyntheticEvent<>) {
// $FlowFixMe TouchableMixin handlers couldn't actually be null
this.touchableHandleResponderTerminate(e);
this.props.onResponderTerminate &&
this.props.onResponderTerminate.apply(this, arguments);
}.bind(this),
onResponderTerminationRequest: function(): boolean {
// Allow touchable or props.onResponderTerminationRequest to deny
// the request
// $FlowFixMe TouchableMixin handlers couldn't actually be null
var allowTermination = this.touchableHandleResponderTerminationRequest();
if (allowTermination && this.props.onResponderTerminationRequest) {
allowTermination = this.props.onResponderTerminationRequest.apply(
this,
arguments,
);
}
return allowTermination;
}.bind(this),
};
}
newProps = {
...this.props,
...this._handlers,
isHighlighted: this.state.isHighlighted,
};
}
if (newProps.selectionColor != null) {
newProps = {
...newProps,
selectionColor: processColor(newProps.selectionColor),
};
}
if (Touchable.TOUCH_TARGET_DEBUG && newProps.onPress) {
newProps = {
...newProps,
style: [this.props.style, {color: 'magenta'}],
};
}
if (this.context.isInAParentText) {
return <RCTVirtualText {...newProps} />;
} else {
if (RCTSimpleText && typeof newProps.children === 'string') {
let simpleProps = {};
Object.assign(simpleProps, newProps);
simpleProps.text = simpleProps.children;
delete simpleProps.children;
return <RCTSimpleText {...simpleProps} />;
}

return <RCTText {...newProps} />;
}
}
}

var RCTText = createReactNativeComponentClass(
viewConfig.uiViewClassName,
() => viewConfig,
);
var RCTVirtualText = RCTText;
var RCTSimpleText;

if (UIManager.RCTVirtualText) {
RCTVirtualText = createReactNativeComponentClass('RCTVirtualText', () => ({
validAttributes: mergeFast(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
}),
uiViewClassName: 'RCTVirtualText',
}));
}

if (UIManager.RCTSimpleText) {
RCTSimpleText = createReactNativeComponentClass(
simpleViewConfig.uiViewClassName,
() => simpleViewConfig,
);
}

module.exports = Text;
1 change: 1 addition & 0 deletions ReactWindows/ReactNative.Net46/ReactNative.Net46.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
<Compile Include="Views\Scroll\ScrollView.cs" />
<Compile Include="Views\Slider\ReactSliderManager.cs" />
<Compile Include="Views\TextInput\PlaceholderAdorner.cs" />
<Compile Include="Views\Text\ReactSimpleTextViewManager.cs" />
<Compile Include="Views\Text\ReactTextCompoundView.cs" />
<Compile Include="Views\Text\ReactTextShadowNode.cs" />
<Compile Include="Views\Text\ReactTextViewManager.cs" />
Expand Down
1 change: 1 addition & 0 deletions ReactWindows/ReactNative.Net46/Shell/MainReactPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public IReadOnlyList<IViewManager> CreateViewManagers(
{
return new List<IViewManager>
{
new ReactSimpleTextViewManager(),
//new ReactFlipViewManager(),
new ReactImageManager(),
new ReactProgressBarViewManager(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Windows.Controls;

namespace ReactNative.Views.Text
{
/// <summary>
/// The view manager for simple text views.
/// </summary>
public class ReactSimpleTextViewManager : ReactTextViewManager
{
/// <summary>
/// Name of the view manager.
/// </summary>
public override string Name
{
get
{
return "RCTSimpleText";
}
}

/// <summary>
/// Gets the number of children for the view.
/// </summary>
/// <param name="parent">The view parent.</param>
/// <returns>The number of children.</returns>
public override int GetChildCount(TextBlock parent)
{
return 0;
}

/// <summary>
/// Removes the children from the view.
/// </summary>
/// <param name="parent">The view parent.</param>
public override void RemoveAllChildren(TextBlock parent)
{
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using ReactNative.UIManager;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

Expand All @@ -13,7 +14,8 @@ public int GetReactTagAtPoint(UIElement reactView, Point point)
{
var richTextBlock = reactView.As<TextBlock>();
var textPointer = richTextBlock.GetPositionFromPoint(point, true);
return textPointer.Parent.GetTag();
var parentView = RootViewHelper.GetReactViewHierarchy(textPointer.Parent).First();
return parentView.GetTag();
}
}
}
24 changes: 22 additions & 2 deletions ReactWindows/ReactNative.Net46/Views/Text/ReactTextShadowNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class ReactTextShadowNode : LayoutShadowNode

private string _fontFamily;

private string _text;

/// <summary>
/// Instantiates a <see cref="ReactTextShadowNode"/>.
/// </summary>
Expand All @@ -39,6 +41,21 @@ public ReactTextShadowNode()
MeasureText(this, node, width, widthMode, height, heightMode);
}

/// <summary>
/// Sets the text for the node.
/// </summary>
/// <param name="text">The text.</param>
[ReactProp("text")]
public void SetText(string text)
{
var nonNullText = text ?? "";
if (_text != nonNullText)
{
_text = nonNullText;
MarkUpdated();
}
}

/// <summary>
/// Sets the font size for the node.
/// </summary>
Expand Down Expand Up @@ -225,8 +242,11 @@ public void UpdateTextBlock(TextBlock textBlock)

private void UpdateTextBlockCore(TextBlock textBlock, bool measureOnly)
{
//textBlock.CharacterSpacing = _letterSpacing;
//textBlock.MaxLines = _numberOfLines;
if (ChildCount == 0 && _text != null)
{
textBlock.Text = _text;
}

textBlock.LineHeight = _lineHeight != 0 ? _lineHeight : double.NaN;
textBlock.TextAlignment = _textAlignment;
textBlock.FontSize = _fontSize ?? 15;
Expand Down
Loading

1 comment on commit 7fa57fc

@rozele
Copy link
Collaborator Author

@rozele rozele commented on 7fa57fc Aug 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI - this commit is labeled prototype, but it's not really a prototype anymore.

Please sign in to comment.