Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/react-native/Libraries/Text/TextEffectNativeComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import type {HostComponent} from '../../src/private/types/HostComponent';

import * as NativeComponentRegistry from '../NativeComponent/NativeComponentRegistry';
import * as React from 'react';

type NativeTextEffectProps = Readonly<{
effectName?: ?string,
effectProps?: ?Readonly<{+[string]: unknown}>,
children?: React.Node,
}>;

const NativeTextEffect: HostComponent<NativeTextEffectProps> =
NativeComponentRegistry.get<NativeTextEffectProps>('RCTTextEffect', () => ({
validAttributes: {
effectName: true,
effectProps: true,
},
uiViewClassName: 'RCTTextEffect',
}));

export default NativeTextEffect;
28 changes: 28 additions & 0 deletions packages/react-native/Libraries/Text/requireNativeTextEffect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import Text from './Text';
import NativeTextEffect from './TextEffectNativeComponent';
import * as React from 'react';

export default function requireNativeTextEffect<P>(
name: string,
): React.ComponentType<{...P, children: React.Node}> {
component TextEffect(...props: {...P, children: React.Node}) {
const {children, ...effectProps} = props;
return (
<NativeTextEffect effectName={name} effectProps={effectProps}>
<Text>{children}</Text>
</NativeTextEffect>
);
}
TextEffect.displayName = `TextEffect(${name})`;
return TextEffect;
}
4 changes: 4 additions & 0 deletions packages/react-native/ReactAndroid/api/ReactAndroid.api
Original file line number Diff line number Diff line change
Expand Up @@ -6141,6 +6141,7 @@ public final class com/facebook/react/views/text/TextAttributeProps {
public static final field TA_KEY_TEXT_DECORATION_COLOR I
public static final field TA_KEY_TEXT_DECORATION_LINE I
public static final field TA_KEY_TEXT_DECORATION_STYLE I
public static final field TA_KEY_TEXT_EFFECTS I
public static final field TA_KEY_TEXT_SHADOW_COLOR I
public static final field TA_KEY_TEXT_SHADOW_OFFSET_DX I
public static final field TA_KEY_TEXT_SHADOW_OFFSET_DY I
Expand Down Expand Up @@ -6209,6 +6210,9 @@ public final class com/facebook/react/views/text/TextAttributes {
public fun toString ()Ljava/lang/String;
}

public final class com/facebook/react/views/text/TextEffectRegistry$Companion {
}

public abstract interface class com/facebook/react/views/textinput/ContentSizeWatcher {
public abstract fun onLayout ()V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
import com.facebook.react.views.text.PreparedLayout;
import com.facebook.react.views.text.ReactTextViewManager;
import com.facebook.react.views.text.ReactTextViewManagerCallback;
import com.facebook.react.views.text.TextEffectRegistry;
import com.facebook.react.views.text.TextLayoutManager;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -179,6 +180,8 @@ public class FabricUIManager
private final MountItemDispatcher mMountItemDispatcher;
private final ViewManagerRegistry mViewManagerRegistry;

private final TextEffectRegistry mTextEffectRegistry = new TextEffectRegistry();

private final BatchEventDispatchedListener mBatchEventDispatchedListener;

private final List<UIManagerListener> mListeners = new CopyOnWriteArrayList<>();
Expand Down Expand Up @@ -553,7 +556,8 @@ private NativeArray measureLines(
PixelUtil.toPixelFromDIP(height),
textViewManager instanceof ReactTextViewManagerCallback
? (ReactTextViewManagerCallback) textViewManager
: null);
: null,
mTextEffectRegistry);
}

public int getColor(int surfaceId, String[] resourcePaths) {
Expand Down Expand Up @@ -641,7 +645,8 @@ public long measureText(
textViewManager instanceof ReactTextViewManagerCallback
? (ReactTextViewManagerCallback) textViewManager
: null,
attachmentsPositions);
attachmentsPositions,
mTextEffectRegistry);
}

@AnyThread
Expand All @@ -666,7 +671,8 @@ public PreparedLayout prepareTextLayout(
getYogaMeasureMode(minHeight, maxHeight),
textViewManager instanceof ReactTextViewManagerCallback
? (ReactTextViewManagerCallback) textViewManager
: null);
: null,
mTextEffectRegistry);
}

@AnyThread
Expand Down Expand Up @@ -700,6 +706,11 @@ public float[] measurePreparedLayout(
getYogaMeasureMode(minHeight, maxHeight));
}

@UnstableReactNativeAPI
public TextEffectRegistry getTextEffectRegistry() {
return mTextEffectRegistry;
}

/**
* @param surfaceId {@link int} surface ID
* @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import androidx.annotation.ColorInt
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
Expand All @@ -28,6 +29,7 @@ import com.facebook.proguard.annotations.DoNotStrip
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.uimanager.BackgroundStyleApplicator
import com.facebook.react.uimanager.ReactCompoundView
import com.facebook.react.uimanager.RootView
import com.facebook.react.uimanager.style.Overflow
import com.facebook.react.views.text.internal.span.AnimatedEffectSpan
import com.facebook.react.views.text.internal.span.CanvasEffectSpan
Expand Down Expand Up @@ -260,6 +262,13 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re
val layoutX = event.x - paddingLeft
val layoutY = event.y - paddingTop - (preparedLayout?.verticalOffset ?: 0f)
if (touchableSpan.onTouchEvent(action, layoutX, layoutY)) {
if (action == MotionEvent.ACTION_DOWN) {
// Returning true from onTouchEvent stops Android's onClickListener path on parents,
// but RN's gesture responder runs at the JS layer and would still let an ancestor
// <Pressable> fire onPress on this gesture. Tell the React root we're taking over so
// it cancels in-flight JS responder tracking — same hook ScrollView uses on intercept.
findRootView()?.onChildStartedNativeGesture(this, event)
}
invalidate()
return true
}
Expand Down Expand Up @@ -290,6 +299,15 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re
return true
}

private fun findRootView(): RootView? {
var p: ViewParent? = parent
while (p != null) {
if (p is RootView) return p
p = p.parent
}
return null
}

private fun <T> getSpanInCoords(x: Int, y: Int, clazz: Class<T>): T? {
val offset = getTextOffsetAt(x, y)
if (offset < 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public constructor(
view.context.assets,
attributedString,
reactTextViewManagerCallback,
TextEffectRegistry.current,
)
view.setSpanned(spanned)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ public class TextAttributeProps private constructor() {
public var role: ReactAccessibilityDelegate.Role? = null
private set

internal data class TextEffectEntry(val name: String, val props: String?)

internal var textEffects: List<TextEffectEntry> = emptyList()
private set

public var fontStyle: Int = ReactConstants.UNSET
private set

Expand Down Expand Up @@ -375,6 +380,9 @@ public class TextAttributeProps private constructor() {
public const val TA_KEY_ROLE: Int = 26
public const val TA_KEY_TEXT_TRANSFORM: Int = 27
public const val TA_KEY_MAX_FONT_SIZE_MULTIPLIER: Int = 29
public const val TA_KEY_TEXT_EFFECTS: Int = 30
private const val TE_KEY_NAME: Int = 0
private const val TE_KEY_PROPS: Int = 1

public const val UNSET: Int = -1

Expand Down Expand Up @@ -429,6 +437,22 @@ public class TextAttributeProps private constructor() {
TA_KEY_TEXT_TRANSFORM -> result.setTextTransform(entry.stringValue)
TA_KEY_MAX_FONT_SIZE_MULTIPLIER ->
result.maxFontSizeMultiplier = entry.doubleValue.toFloat()
TA_KEY_TEXT_EFFECTS -> {
val effectsMap = entry.mapBufferValue
val list = mutableListOf<TextEffectEntry>()
for (j in 0 until effectsMap.count) {
val effectMap = effectsMap.getMapBuffer(j)
list.add(
TextEffectEntry(
name = effectMap.getString(TE_KEY_NAME),
props =
if (effectMap.contains(TE_KEY_PROPS)) effectMap.getString(TE_KEY_PROPS)
else null,
)
)
}
result.textEffects = list
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.text

import com.facebook.common.logging.FLog
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.views.text.internal.span.TextEffectSpan
import java.util.concurrent.ConcurrentHashMap

@UnstableReactNativeAPI
public class TextEffectRegistry {
private val factories = ConcurrentHashMap<String, TextEffectSpanFactory>()

public fun register(name: String, factory: TextEffectSpanFactory) {
factories[name] = factory
current = this
}

public fun unregister(name: String) {
factories.remove(name)
}

internal fun createSpan(name: String, props: ReadableMap?): TextEffectSpan? {
val factory = factories[name] ?: return null
return try {
factory.createSpan(props)
} catch (t: Throwable) {
// A throwing factory (e.g. invalid color prop) must not break the entire text render path
// for an unrelated paragraph or for subsequent renders. Skip this span and keep going.
FLog.e(TAG, "TextEffectSpanFactory '$name' threw — skipping span", t)
null
}
}

public companion object {
private const val TAG = "TextEffectRegistry"
@JvmField @Volatile public var current: TextEffectRegistry? = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.views.text

import com.facebook.react.bridge.ReadableMap
import com.facebook.react.common.annotations.UnstableReactNativeAPI
import com.facebook.react.views.text.internal.span.TextEffectSpan

@UnstableReactNativeAPI
public fun interface TextEffectSpanFactory {
public fun createSpan(props: ReadableMap?): TextEffectSpan
}
Loading
Loading