diff --git a/packages/react-native/Libraries/Text/TextEffectNativeComponent.js b/packages/react-native/Libraries/Text/TextEffectNativeComponent.js new file mode 100644 index 000000000000..f11d5ecfd5aa --- /dev/null +++ b/packages/react-native/Libraries/Text/TextEffectNativeComponent.js @@ -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 = + NativeComponentRegistry.get('RCTTextEffect', () => ({ + validAttributes: { + effectName: true, + effectProps: true, + }, + uiViewClassName: 'RCTTextEffect', + })); + +export default NativeTextEffect; diff --git a/packages/react-native/Libraries/Text/requireNativeTextEffect.js b/packages/react-native/Libraries/Text/requireNativeTextEffect.js new file mode 100644 index 000000000000..cc15e5762ae1 --- /dev/null +++ b/packages/react-native/Libraries/Text/requireNativeTextEffect.js @@ -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

( + name: string, +): React.ComponentType<{...P, children: React.Node}> { + component TextEffect(...props: {...P, children: React.Node}) { + const {children, ...effectProps} = props; + return ( + + {children} + + ); + } + TextEffect.displayName = `TextEffect(${name})`; + return TextEffect; +} diff --git a/packages/react-native/ReactAndroid/api/ReactAndroid.api b/packages/react-native/ReactAndroid/api/ReactAndroid.api index c2d6a186fa6a..cc3f6b5e9448 100644 --- a/packages/react-native/ReactAndroid/api/ReactAndroid.api +++ b/packages/react-native/ReactAndroid/api/ReactAndroid.api @@ -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 @@ -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 } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 066dd9d8b29e..2a0baaeace09 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -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; @@ -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 mListeners = new CopyOnWriteArrayList<>(); @@ -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) { @@ -641,7 +645,8 @@ public long measureText( textViewManager instanceof ReactTextViewManagerCallback ? (ReactTextViewManagerCallback) textViewManager : null, - attachmentsPositions); + attachmentsPositions, + mTextEffectRegistry); } @AnyThread @@ -666,7 +671,8 @@ public PreparedLayout prepareTextLayout( getYogaMeasureMode(minHeight, maxHeight), textViewManager instanceof ReactTextViewManagerCallback ? (ReactTextViewManagerCallback) textViewManager - : null); + : null, + mTextEffectRegistry); } @AnyThread @@ -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 diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt index 427e71e059f1..21609f7fd4f7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/PreparedLayoutTextView.kt @@ -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 @@ -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 @@ -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 + // 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 } @@ -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 getSpanInCoords(x: Int, y: Int, clazz: Class): T? { val offset = getTextOffsetAt(x, y) if (offset < 0) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt index b5696f22533d..bc420d79c67f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.kt @@ -160,6 +160,7 @@ public constructor( view.context.assets, attributedString, reactTextViewManagerCallback, + TextEffectRegistry.current, ) view.setSpanned(spanned) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt index 45c8bec4c2b8..abc28bc09483 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.kt @@ -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 = emptyList() + private set + public var fontStyle: Int = ReactConstants.UNSET private set @@ -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 @@ -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() + 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 + } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextEffectRegistry.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextEffectRegistry.kt new file mode 100644 index 000000000000..7f727381f5c0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextEffectRegistry.kt @@ -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() + + 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 + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextEffectSpanFactory.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextEffectSpanFactory.kt new file mode 100644 index 000000000000..5311c3df4bb0 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextEffectSpanFactory.kt @@ -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 +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt index eacec697670d..b5d061823f9b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.kt @@ -26,8 +26,12 @@ import android.view.Gravity import android.view.View import com.facebook.common.logging.FLog import com.facebook.infer.annotation.Assertions +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableArray import com.facebook.react.common.ReactConstants +import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.common.mapbuffer.MapBuffer import com.facebook.react.common.mapbuffer.ReadableMapBuffer import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags @@ -60,6 +64,8 @@ import kotlin.math.ceil import kotlin.math.floor import kotlin.math.max import kotlin.math.min +import org.json.JSONArray +import org.json.JSONObject /** Class responsible of creating [Spanned] object for the JS representation of Text */ internal object TextLayoutManager { @@ -229,13 +235,20 @@ internal object TextLayoutManager { } } + @OptIn(UnstableReactNativeAPI::class) private fun buildSpannableFromFragments( assets: AssetManager, fragments: MapBuffer, sb: SpannableStringBuilder, ops: MutableList, outputReactTags: IntArray?, + textEffectRegistry: TextEffectRegistry?, ) { + // Track pending text effects to coalesce consecutive fragments with the same effects into + // single spans, avoiding duplicate draws (e.g. multiple accent marks in HighlighterTextSpan). + var pendingEffects: List = emptyList() + var pendingEffectStart = 0 + for (i in 0 until fragments.count) { val fragment = fragments.getMapBuffer(i) val start = sb.length @@ -352,6 +365,34 @@ internal object TextLayoutManager { ops.add(SetSpanOperation(start, end, ReactTagSpan(reactTag))) } } + + // Coalesce consecutive fragments with the same text effects into single spans. + val effects = textAttributes.textEffects + if (effects != pendingEffects) { + // Flush the previous pending effects + if (pendingEffects.isNotEmpty() && textEffectRegistry != null) { + for (effect in pendingEffects) { + val effectProps = jsonStringToReadableMap(effect.props) + val span = textEffectRegistry.createSpan(effect.name, effectProps) + if (span != null) { + ops.add(SetSpanOperation(pendingEffectStart, start, span)) + } + } + } + pendingEffects = effects + pendingEffectStart = start + } + } + + // Flush any remaining pending effects after the last fragment + if (pendingEffects.isNotEmpty() && textEffectRegistry != null) { + for (effect in pendingEffects) { + val effectProps = jsonStringToReadableMap(effect.props) + val span = textEffectRegistry.createSpan(effect.name, effectProps) + if (span != null) { + ops.add(SetSpanOperation(pendingEffectStart, sb.length, span)) + } + } } } @@ -364,10 +405,12 @@ internal object TextLayoutManager { val height: Double, ) + @OptIn(UnstableReactNativeAPI::class) private fun buildSpannableFromFragmentsOptimized( assets: AssetManager, fragments: MapBuffer, outputReactTags: IntArray?, + textEffectRegistry: TextEffectRegistry?, ): Spannable { val text = StringBuilder() val parsedFragments = ArrayList(fragments.count) @@ -408,6 +451,11 @@ internal object TextLayoutManager { val spannable = SpannableString(text) + // Track pending text effects to coalesce consecutive fragments with the same effects into + // single spans, avoiding duplicate draws (e.g. multiple accent marks in HighlighterTextSpan). + var pendingEffects: List = emptyList() + var pendingEffectStart = 0 + var start = 0 for ((i, fragment) in parsedFragments.withIndex()) { val end = start + fragment.length @@ -534,16 +582,53 @@ internal object TextLayoutManager { } } + // Coalesce consecutive fragments with the same text effects into single spans. + val effects = fragment.props.textEffects + if (effects != pendingEffects) { + if (pendingEffects.isNotEmpty() && textEffectRegistry != null) { + for (effect in pendingEffects) { + val effectProps = jsonStringToReadableMap(effect.props) + val span = textEffectRegistry.createSpan(effect.name, effectProps) + if (span != null) { + spannable.setSpan(span, pendingEffectStart, start, Spannable.SPAN_EXCLUSIVE_INCLUSIVE) + } + } + } + pendingEffects = effects + pendingEffectStart = start + } + start = end } + // Flush any remaining pending effects after the last fragment + if (pendingEffects.isNotEmpty() && textEffectRegistry != null) { + for (effect in pendingEffects) { + val effectProps = jsonStringToReadableMap(effect.props) + val span = textEffectRegistry.createSpan(effect.name, effectProps) + if (span != null) { + spannable.setSpan(span, pendingEffectStart, start, Spannable.SPAN_EXCLUSIVE_INCLUSIVE) + } + } + } + return spannable } + @OptIn(UnstableReactNativeAPI::class) fun getOrCreateSpannableForText( assets: AssetManager, attributedString: MapBuffer, reactTextViewManagerCallback: ReactTextViewManagerCallback?, + ): Spannable = + getOrCreateSpannableForText(assets, attributedString, reactTextViewManagerCallback, null) + + @OptIn(UnstableReactNativeAPI::class) + internal fun getOrCreateSpannableForText( + assets: AssetManager, + attributedString: MapBuffer, + reactTextViewManagerCallback: ReactTextViewManagerCallback?, + textEffectRegistry: TextEffectRegistry?, ): Spannable { var text: Spannable? if (attributedString.contains(AS_KEY_CACHE_ID)) { @@ -556,20 +641,29 @@ internal object TextLayoutManager { attributedString.getMapBuffer(AS_KEY_FRAGMENTS), reactTextViewManagerCallback, null, + textEffectRegistry, ) } return text } + @OptIn(UnstableReactNativeAPI::class) private fun createSpannableFromAttributedString( assets: AssetManager, fragments: MapBuffer, reactTextViewManagerCallback: ReactTextViewManagerCallback?, outputReactTags: IntArray?, + textEffectRegistry: TextEffectRegistry? = null, ): Spannable { if (ReactNativeFeatureFlags.enableAndroidTextMeasurementOptimizations()) { - val spannable = buildSpannableFromFragmentsOptimized(assets, fragments, outputReactTags) + val spannable = + buildSpannableFromFragmentsOptimized( + assets, + fragments, + outputReactTags, + textEffectRegistry, + ) reactTextViewManagerCallback?.onPostProcessSpannable(spannable) return spannable @@ -581,7 +675,7 @@ internal object TextLayoutManager { // a new spannable will be wiped out val ops: MutableList = ArrayList() - buildSpannableFromFragments(assets, fragments, sb, ops, outputReactTags) + buildSpannableFromFragments(assets, fragments, sb, ops, outputReactTags, textEffectRegistry) // TODO T31905686: add support for inline Images // While setting the Spans on the final text, we also check whether any of them are images. @@ -759,6 +853,7 @@ internal object TextLayoutManager { return paint } + @OptIn(UnstableReactNativeAPI::class) private fun createLayoutForMeasurement( assets: AssetManager, attributedString: MapBuffer, @@ -768,8 +863,15 @@ internal object TextLayoutManager { height: Float, heightYogaMeasureMode: YogaMeasureMode, reactTextViewManagerCallback: ReactTextViewManagerCallback?, + textEffectRegistry: TextEffectRegistry? = null, ): Layout { - val text = getOrCreateSpannableForText(assets, attributedString, reactTextViewManagerCallback) + val text = + getOrCreateSpannableForText( + assets, + attributedString, + reactTextViewManagerCallback, + textEffectRegistry, + ) val paint: TextPaint if (attributedString.contains(AS_KEY_CACHE_ID)) { @@ -881,6 +983,7 @@ internal object TextLayoutManager { } @JvmStatic + @OptIn(UnstableReactNativeAPI::class) fun createPreparedLayout( assets: AssetManager, attributedString: ReadableMapBuffer, @@ -890,6 +993,7 @@ internal object TextLayoutManager { height: Float, heightYogaMeasureMode: YogaMeasureMode, reactTextViewManagerCallback: ReactTextViewManagerCallback?, + textEffectRegistry: TextEffectRegistry? = null, ): PreparedLayout { val fragments = attributedString.getMapBuffer(AS_KEY_FRAGMENTS) val reactTags = IntArray(fragments.count) @@ -899,6 +1003,7 @@ internal object TextLayoutManager { fragments, reactTextViewManagerCallback, reactTags, + textEffectRegistry, ) val baseTextAttributes = TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES)) @@ -1044,6 +1149,7 @@ internal object TextLayoutManager { } @JvmStatic + @OptIn(UnstableReactNativeAPI::class) fun measureText( assets: AssetManager, attributedString: MapBuffer, @@ -1054,6 +1160,7 @@ internal object TextLayoutManager { heightYogaMeasureMode: YogaMeasureMode, reactTextViewManagerCallback: ReactTextViewManagerCallback?, attachmentsPositions: FloatArray?, + textEffectRegistry: TextEffectRegistry? = null, ): Long { // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic) val layout = @@ -1066,6 +1173,7 @@ internal object TextLayoutManager { height, heightYogaMeasureMode, reactTextViewManagerCallback, + textEffectRegistry, ) val maximumNumberOfLines = @@ -1314,6 +1422,7 @@ internal object TextLayoutManager { } @JvmStatic + @OptIn(UnstableReactNativeAPI::class) fun measureLines( assetManager: AssetManager, attributedString: MapBuffer, @@ -1321,6 +1430,7 @@ internal object TextLayoutManager { width: Float, height: Float, reactTextViewManagerCallback: ReactTextViewManagerCallback?, + textEffectRegistry: TextEffectRegistry? = null, ): WritableArray { val layout = createLayoutForMeasurement( @@ -1332,6 +1442,7 @@ internal object TextLayoutManager { height, YogaMeasureMode.EXACTLY, reactTextViewManagerCallback, + textEffectRegistry, ) return FontMetricsUtil.getFontMetrics( layout.text, @@ -1372,4 +1483,63 @@ internal object TextLayoutManager { var width: Float = 0f var height: Float = 0f } + + private fun jsonStringToReadableMap(json: String?): ReadableMap? { + if (json == null) return null + return try { + jsonObjectToReadableMap(JSONObject(json)) + } catch (_: Exception) { + null + } + } + + private fun jsonObjectToReadableMap(jsonObject: JSONObject): JavaOnlyMap { + val map = JavaOnlyMap() + val keys = jsonObject.keys() + while (keys.hasNext()) { + val key = keys.next() + putJsonValue(map, key, jsonObject.get(key)) + } + return map + } + + private fun jsonArrayToReadableArray(jsonArray: JSONArray): JavaOnlyArray { + val array = JavaOnlyArray() + for (i in 0 until jsonArray.length()) { + pushJsonValue(array, jsonArray.get(i)) + } + return array + } + + private fun putJsonValue(map: JavaOnlyMap, key: String, value: Any) { + when (value) { + JSONObject.NULL -> map.putNull(key) + is Boolean -> map.putBoolean(key, value) + is Number -> map.putDouble(key, value.toDouble()) + is String -> map.putString(key, value) + is JSONObject -> map.putMap(key, jsonObjectToReadableMap(value)) + is JSONArray -> map.putArray(key, jsonArrayToReadableArray(value)) + else -> + FLog.w( + ReactConstants.TAG, + "Unsupported text effect prop type for key $key: ${value.javaClass.name}", + ) + } + } + + private fun pushJsonValue(array: JavaOnlyArray, value: Any) { + when (value) { + JSONObject.NULL -> array.pushNull() + is Boolean -> array.pushBoolean(value) + is Number -> array.pushDouble(value.toDouble()) + is String -> array.pushString(value) + is JSONObject -> array.pushMap(jsonObjectToReadableMap(value)) + is JSONArray -> array.pushArray(jsonArrayToReadableArray(value)) + else -> + FLog.w( + ReactConstants.TAG, + "Unsupported text effect prop type: ${value.javaClass.name}", + ) + } + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CanvasEffectSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CanvasEffectSpan.kt index 1ec5761780d6..6f070b56fdf4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CanvasEffectSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/CanvasEffectSpan.kt @@ -14,7 +14,7 @@ import android.text.Layout * A span which draws a static effect on top of text. [onPreDraw] and [onDraw] hooks are called * during [PreparedLayoutTextView] drawing, providing glyph layout information for custom rendering. */ -public abstract class CanvasEffectSpan { +public abstract class CanvasEffectSpan : TextEffectSpan { /** * Called before the text is drawn. This happens after the Paragraph component has drawn its * background, but may be called before text spans with their own background color are drawn. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/SetSpanOperation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/SetSpanOperation.kt index 72cd31b140be..aedb175c318b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/SetSpanOperation.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/SetSpanOperation.kt @@ -16,7 +16,7 @@ import kotlin.math.max internal class SetSpanOperation( private val start: Int, private val end: Int, - @JvmField val what: ReactSpan, + @JvmField val what: Any, ) { /** * @param builder Spannable string builder diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StatefulSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StatefulSpan.kt index cc8f09284efa..53cc50d77fad 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StatefulSpan.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/StatefulSpan.kt @@ -15,7 +15,7 @@ import com.facebook.react.common.annotations.UnstableReactNativeAPI * spannable so that each view gets independent state even when layouts are shared from a cache. */ @UnstableReactNativeAPI -public interface StatefulSpan { +public interface StatefulSpan : TextEffectSpan { /** Returns a fresh instance with the same configuration but independent mutable state. */ public fun clone(): StatefulSpan } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/TextEffectSpan.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/TextEffectSpan.kt new file mode 100644 index 000000000000..685ebc338d9f --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/TextEffectSpan.kt @@ -0,0 +1,19 @@ +/* + * 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.internal.span + +/** + * Marker interface implemented by every span that may be returned from a + * [com.facebook.react.views.text.TextEffectSpanFactory]. Constraining the factory's return type to + * this interface means a `List` (or any other non-span value) is rejected at compile time, which + * was previously possible because the contract was typed as `Any`. + * + * Built-in span families ([CanvasEffectSpan], [StatefulSpan], including [AnimatedEffectSpan]) + * implement this directly so that user spans extending them satisfy the contract automatically. + */ +public interface TextEffectSpan diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp index f6d84433adf1..57c1e0e8d86e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/CoreComponentsRegistry.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -61,6 +62,8 @@ void addCoreComponents( concreteComponentDescriptorProvider()); providerRegistry->add( concreteComponentDescriptorProvider()); + providerRegistry->add( + concreteComponentDescriptorProvider()); providerRegistry->add( concreteComponentDescriptorProvider()); providerRegistry->add( diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp index 69ea8c82fc3f..be2e1a2d481b 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp @@ -114,6 +114,8 @@ void TextAttributes::apply(TextAttributes textAttributes) { ? textAttributes.accessibilityRole : accessibilityRole; role = textAttributes.role.has_value() ? textAttributes.role : role; + textEffects = !textAttributes.textEffects.empty() ? textAttributes.textEffects + : textEffects; } #pragma mark - Operators @@ -141,7 +143,8 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { layoutDirection, accessibilityRole, role, - textTransform) == + textTransform, + textEffects) == std::tie( rhs.foregroundColor, rhs.backgroundColor, @@ -164,7 +167,8 @@ bool TextAttributes::operator==(const TextAttributes& rhs) const { rhs.layoutDirection, rhs.accessibilityRole, rhs.role, - rhs.textTransform) && + rhs.textTransform, + rhs.textEffects) && floatEquality(maxFontSizeMultiplier, rhs.maxFontSizeMultiplier) && floatEquality(opacity, rhs.opacity) && floatEquality(fontSize, rhs.fontSize) && diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h index b66452408b75..c162deee2f73 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/TextAttributes.h @@ -10,7 +10,9 @@ #include #include #include +#include +#include #include #include #include @@ -23,6 +25,12 @@ namespace facebook::react { +struct TextEffectInfo { + std::string name; + folly::dynamic props; + bool operator==(const TextEffectInfo &) const = default; +}; + class TextAttributes; using SharedTextAttributes = std::shared_ptr; @@ -86,6 +94,9 @@ class TextAttributes : public DebugStringConvertible { std::optional accessibilityRole{}; std::optional role{}; + // Text Effects (ordered by nesting depth: index 0 = outermost = drawn first) + std::vector textEffects{}; + #pragma mark - Operations void apply(TextAttributes textAttributes); @@ -105,10 +116,22 @@ class TextAttributes : public DebugStringConvertible { namespace std { +template <> +struct hash { + size_t operator()(const facebook::react::TextEffectInfo &info) const + { + return facebook::react::hash_combine(info.name, info.props); + } +}; + template <> struct hash { size_t operator()(const facebook::react::TextAttributes &textAttributes) const { + size_t textEffectsHash = 0; + for (const auto &effect : textAttributes.textEffects) { + facebook::react::hash_combine(textEffectsHash, effect); + } return facebook::react::hash_combine( textAttributes.foregroundColor, textAttributes.backgroundColor, @@ -138,7 +161,8 @@ struct hash { textAttributes.isPressable, textAttributes.layoutDirection, textAttributes.accessibilityRole, - textAttributes.role); + textAttributes.role, + textEffectsHash); } }; } // namespace std diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 331e2019338a..9fabf68999cf 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -26,6 +26,7 @@ #include #ifdef RN_SERIALIZABLE_STATE +#include #include #include #endif @@ -1128,6 +1129,11 @@ constexpr static MapBuffer::Key TA_KEY_ROLE = 26; constexpr static MapBuffer::Key TA_KEY_TEXT_TRANSFORM = 27; constexpr static MapBuffer::Key TA_KEY_ALIGNMENT_VERTICAL = 28; constexpr static MapBuffer::Key TA_KEY_MAX_FONT_SIZE_MULTIPLIER = 29; +constexpr static MapBuffer::Key TA_KEY_TEXT_EFFECTS = 30; + +// Keys within each text effect entry MapBuffer +constexpr static MapBuffer::Key TE_KEY_NAME = 0; +constexpr static MapBuffer::Key TE_KEY_PROPS = 1; // constants for ParagraphAttributes serialization constexpr static MapBuffer::Key PA_KEY_MAX_NUMBER_OF_LINES = 0; @@ -1332,6 +1338,16 @@ inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) if (textAttributes.role.has_value()) { builder.putInt(TA_KEY_ROLE, static_cast(*textAttributes.role)); } + if (!textAttributes.textEffects.empty()) { + auto effectsBuilder = MapBufferBuilder(); + for (size_t i = 0; i < textAttributes.textEffects.size(); i++) { + auto effectBuilder = MapBufferBuilder(); + effectBuilder.putString(TE_KEY_NAME, textAttributes.textEffects[i].name); + effectBuilder.putString(TE_KEY_PROPS, folly::toJson(textAttributes.textEffects[i].props)); + effectsBuilder.putMapBuffer(i, effectBuilder.build()); + } + builder.putMapBuffer(TA_KEY_TEXT_EFFECTS, effectsBuilder.build()); + } return builder.build(); } diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp index 7050b8406efa..4c0ca0070dba 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/BaseTextShadowNode.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -70,6 +71,24 @@ void BaseTextShadowNode::buildAttributedString( continue; } + // TextEffectShadowNode + auto textEffectNode = + dynamic_cast(childNode.get()); + if (textEffectNode != nullptr) { + auto localTextAttributes = baseTextAttributes; + const auto& effectProps = textEffectNode->getConcreteProps(); + localTextAttributes.textEffects.push_back( + TextEffectInfo{ + .name = effectProps.effectName, + .props = effectProps.effectProps}); + buildAttributedString( + localTextAttributes, + *textEffectNode, + outAttributedString, + outAttachments); + continue; + } + // Any *other* kind of ShadowNode auto fragment = AttributedString::Fragment{}; fragment.string = AttributedString::Fragment::AttachmentCharacter(); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectComponentDescriptor.h b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectComponentDescriptor.h new file mode 100644 index 000000000000..4fb2917f0ae6 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectComponentDescriptor.h @@ -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. + */ + +#pragma once + +#include +#include + +namespace facebook::react { + +using TextEffectComponentDescriptor = ConcreteComponentDescriptor; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectProps.cpp new file mode 100644 index 000000000000..8738ed38fd8f --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectProps.cpp @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#include "TextEffectProps.h" + +#include + +namespace facebook::react { + +TextEffectProps::TextEffectProps( + const PropsParserContext& context, + const TextEffectProps& sourceProps, + const RawProps& rawProps) + : Props(context, sourceProps, rawProps), + effectName(convertRawProp( + context, + rawProps, + "effectName", + sourceProps.effectName, + std::string{})), + effectProps(convertRawProp( + context, + rawProps, + "effectProps", + sourceProps.effectProps, + folly::dynamic(nullptr))) {} + +#ifdef RN_SERIALIZABLE_STATE + +ComponentName TextEffectProps::getDiffPropsImplementationTarget() const { + return "TextEffect"; +} + +folly::dynamic TextEffectProps::getDiffProps(const Props* prevProps) const { + folly::dynamic result = folly::dynamic::object(); + + static const auto defaultProps = TextEffectProps(); + + const TextEffectProps* oldProps = prevProps == nullptr + ? &defaultProps + : static_cast(prevProps); + + if (effectName != oldProps->effectName) { + result["effectName"] = effectName; + } + if (effectProps != oldProps->effectProps) { + result["effectProps"] = effectProps; + } + + return result; +} + +#endif + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectProps.h b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectProps.h new file mode 100644 index 000000000000..42cb2ffa62db --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectProps.h @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +class TextEffectProps : public Props { + public: + TextEffectProps() = default; + TextEffectProps(const PropsParserContext &context, const TextEffectProps &sourceProps, const RawProps &rawProps); + + std::string effectName; + folly::dynamic effectProps; + +#ifdef RN_SERIALIZABLE_STATE + ComponentName getDiffPropsImplementationTarget() const override; + folly::dynamic getDiffProps(const Props *prevProps) const override; +#endif +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectShadowNode.cpp new file mode 100644 index 000000000000..d0f5e0380851 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectShadowNode.cpp @@ -0,0 +1,14 @@ +/* + * 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. + */ + +#include "TextEffectShadowNode.h" + +namespace facebook::react { + +extern const char TextEffectComponentName[] = "TextEffect"; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectShadowNode.h new file mode 100644 index 000000000000..60185ce039b9 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/text/TextEffectShadowNode.h @@ -0,0 +1,26 @@ +/* + * 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. + */ + +#pragma once + +#include +#include +#include + +namespace facebook::react { + +extern const char TextEffectComponentName[]; + +using TextEffectEventEmitter = TouchEventEmitter; + +class TextEffectShadowNode + : public ConcreteShadowNode { + public: + using ConcreteShadowNode::ConcreteShadowNode; +}; + +} // namespace facebook::react diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 54d994781de4..42ad710f2bb7 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -456,6 +456,7 @@ const char facebook::react::RuntimeSchedulerKey[]; const char facebook::react::SafeAreaViewComponentName[]; const char facebook::react::ScrollViewComponentName[]; const char facebook::react::TextComponentName[]; +const char facebook::react::TextEffectComponentName[]; const char facebook::react::UnimplementedViewComponentName[]; const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; @@ -521,11 +522,14 @@ static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_ROLE; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_DECORATION_COLOR; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_DECORATION_LINE; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_DECORATION_STYLE; +static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_EFFECTS; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_SHADOW_COLOR; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_SHADOW_OFFSET_DX; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_SHADOW_OFFSET_DY; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_SHADOW_RADIUS; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_TRANSFORM; +static constexpr facebook::react::MapBuffer::Key facebook::react::TE_KEY_NAME; +static constexpr facebook::react::MapBuffer::Key facebook::react::TE_KEY_PROPS; static constexpr facebook::react::MapBuffer::Key facebook::react::TX_STATE_KEY_ATTRIBUTED_STRING; static constexpr facebook::react::MapBuffer::Key facebook::react::TX_STATE_KEY_HASH; static constexpr facebook::react::MapBuffer::Key facebook::react::TX_STATE_KEY_MOST_RECENT_EVENT_COUNT; @@ -691,6 +695,8 @@ using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; using facebook::react::TelemetryTimePoint = facebook::react::TelemetryClock::time_point; using facebook::react::TextComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextLayoutManagerExtended = facebook::react::detail::TextLayoutManagerExtended; using facebook::react::TextMeasureCache = facebook::react::SimpleThreadSafeCache; @@ -4950,9 +4956,22 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textTransform; public std::optional baseWritingDirection; public std::string fontFamily; + public std::vector textEffects; public void apply(facebook::react::TextAttributes textAttributes); } +class facebook::react::TextEffectProps : public facebook::react::Props { + public TextEffectProps() = default; + public TextEffectProps(const facebook::react::PropsParserContext& context, const facebook::react::TextEffectProps& sourceProps, const facebook::react::RawProps& rawProps); + public folly::dynamic effectProps; + public std::string effectName; + public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; + public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; +} + +class facebook::react::TextEffectShadowNode : public facebook::react::ConcreteShadowNode { +} + class facebook::react::TextInputEventEmitter : public facebook::react::BaseViewEventEmitter { public void onBlur(const facebook::react::TextInputEventEmitter::Metrics& textInputMetrics) const; public void onChange(const facebook::react::TextInputEventEmitter::Metrics& textInputMetrics) const; @@ -7900,6 +7919,12 @@ struct facebook::react::Task : public facebook::jsi::NativeState { public Task(facebook::react::SchedulerPriority priority, facebook::react::RawCallback&& callback, facebook::react::HighResTimeStamp expirationTime); } +struct facebook::react::TextEffectInfo { + public bool operator==(const facebook::react::TextEffectInfo&) const = default; + public folly::dynamic props; + public std::string name; +} + struct facebook::react::TextLayoutContext { public bool operator==(const facebook::react::TextLayoutContext& rhs) const = default; public facebook::react::Float pointScaleFactor; @@ -13632,6 +13657,10 @@ struct std::hash { public size_t operator()(const facebook::react::TextAttributes& textAttributes) const; } +struct std::hash { + public size_t operator()(const facebook::react::TextEffectInfo& info) const; +} + struct std::hash { public size_t operator()(const facebook::react::TextMeasureCacheKey& key) const; } diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 6a0a48343467..8617a4982145 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -456,6 +456,7 @@ const char facebook::react::RuntimeSchedulerKey[]; const char facebook::react::SafeAreaViewComponentName[]; const char facebook::react::ScrollViewComponentName[]; const char facebook::react::TextComponentName[]; +const char facebook::react::TextEffectComponentName[]; const char facebook::react::UnimplementedViewComponentName[]; const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; @@ -521,11 +522,14 @@ static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_ROLE; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_DECORATION_COLOR; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_DECORATION_LINE; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_DECORATION_STYLE; +static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_EFFECTS; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_SHADOW_COLOR; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_SHADOW_OFFSET_DX; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_SHADOW_OFFSET_DY; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_SHADOW_RADIUS; static constexpr facebook::react::MapBuffer::Key facebook::react::TA_KEY_TEXT_TRANSFORM; +static constexpr facebook::react::MapBuffer::Key facebook::react::TE_KEY_NAME; +static constexpr facebook::react::MapBuffer::Key facebook::react::TE_KEY_PROPS; static constexpr facebook::react::MapBuffer::Key facebook::react::TX_STATE_KEY_ATTRIBUTED_STRING; static constexpr facebook::react::MapBuffer::Key facebook::react::TX_STATE_KEY_HASH; static constexpr facebook::react::MapBuffer::Key facebook::react::TX_STATE_KEY_MOST_RECENT_EVENT_COUNT; @@ -691,6 +695,8 @@ using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; using facebook::react::TelemetryTimePoint = facebook::react::TelemetryClock::time_point; using facebook::react::TextComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextLayoutManagerExtended = facebook::react::detail::TextLayoutManagerExtended; using facebook::react::TextMeasureCache = facebook::react::SimpleThreadSafeCache; @@ -4941,9 +4947,22 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textTransform; public std::optional baseWritingDirection; public std::string fontFamily; + public std::vector textEffects; public void apply(facebook::react::TextAttributes textAttributes); } +class facebook::react::TextEffectProps : public facebook::react::Props { + public TextEffectProps() = default; + public TextEffectProps(const facebook::react::PropsParserContext& context, const facebook::react::TextEffectProps& sourceProps, const facebook::react::RawProps& rawProps); + public folly::dynamic effectProps; + public std::string effectName; + public virtual facebook::react::ComponentName getDiffPropsImplementationTarget() const override; + public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; +} + +class facebook::react::TextEffectShadowNode : public facebook::react::ConcreteShadowNode { +} + class facebook::react::TextInputEventEmitter : public facebook::react::BaseViewEventEmitter { public void onBlur(const facebook::react::TextInputEventEmitter::Metrics& textInputMetrics) const; public void onChange(const facebook::react::TextInputEventEmitter::Metrics& textInputMetrics) const; @@ -7891,6 +7910,12 @@ struct facebook::react::Task : public facebook::jsi::NativeState { public Task(facebook::react::SchedulerPriority priority, facebook::react::RawCallback&& callback, facebook::react::HighResTimeStamp expirationTime); } +struct facebook::react::TextEffectInfo { + public bool operator==(const facebook::react::TextEffectInfo&) const = default; + public folly::dynamic props; + public std::string name; +} + struct facebook::react::TextLayoutContext { public bool operator==(const facebook::react::TextLayoutContext& rhs) const = default; public facebook::react::Float pointScaleFactor; @@ -13488,6 +13513,10 @@ struct std::hash { public size_t operator()(const facebook::react::TextAttributes& textAttributes) const; } +struct std::hash { + public size_t operator()(const facebook::react::TextEffectInfo& info) const; +} + struct std::hash { public size_t operator()(const facebook::react::TextMeasureCacheKey& key) const; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index bb3a74b3522e..fdbc699282d8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -3735,6 +3735,7 @@ const char facebook::react::RuntimeSchedulerKey[]; const char facebook::react::SafeAreaViewComponentName[]; const char facebook::react::ScrollViewComponentName[]; const char facebook::react::TextComponentName[]; +const char facebook::react::TextEffectComponentName[]; const char facebook::react::TextInputComponentName[]; const char facebook::react::UnimplementedViewComponentName[]; const char facebook::react::ViewComponentName[]; @@ -3914,6 +3915,8 @@ using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; using facebook::react::TelemetryTimePoint = facebook::react::TelemetryClock::time_point; using facebook::react::TextComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextLayoutManagerExtended = facebook::react::detail::TextLayoutManagerExtended; using facebook::react::TextMeasureCache = facebook::react::SimpleThreadSafeCache; @@ -7511,9 +7514,20 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textTransform; public std::optional baseWritingDirection; public std::string fontFamily; + public std::vector textEffects; public void apply(facebook::react::TextAttributes textAttributes); } +class facebook::react::TextEffectProps : public facebook::react::Props { + public TextEffectProps() = default; + public TextEffectProps(const facebook::react::PropsParserContext& context, const facebook::react::TextEffectProps& sourceProps, const facebook::react::RawProps& rawProps); + public folly::dynamic effectProps; + public std::string effectName; +} + +class facebook::react::TextEffectShadowNode : public facebook::react::ConcreteShadowNode { +} + class facebook::react::TextInputComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { protected virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; public TextInputComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); @@ -10311,6 +10325,12 @@ struct facebook::react::Task : public facebook::jsi::NativeState { public Task(facebook::react::SchedulerPriority priority, facebook::react::RawCallback&& callback, facebook::react::HighResTimeStamp expirationTime); } +struct facebook::react::TextEffectInfo { + public bool operator==(const facebook::react::TextEffectInfo&) const = default; + public folly::dynamic props; + public std::string name; +} + struct facebook::react::TextLayoutContext { public bool operator==(const facebook::react::TextLayoutContext& rhs) const = default; public facebook::react::Float pointScaleFactor; @@ -16165,6 +16185,10 @@ struct std::hash { public size_t operator()(const facebook::react::TextAttributes& textAttributes) const; } +struct std::hash { + public size_t operator()(const facebook::react::TextEffectInfo& info) const; +} + struct std::hash { public size_t operator()(const facebook::react::TextMeasureCacheKey& key) const; } diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 53f03430fc4b..f2915c5e97a8 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -3735,6 +3735,7 @@ const char facebook::react::RuntimeSchedulerKey[]; const char facebook::react::SafeAreaViewComponentName[]; const char facebook::react::ScrollViewComponentName[]; const char facebook::react::TextComponentName[]; +const char facebook::react::TextEffectComponentName[]; const char facebook::react::TextInputComponentName[]; const char facebook::react::UnimplementedViewComponentName[]; const char facebook::react::ViewComponentName[]; @@ -3914,6 +3915,8 @@ using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; using facebook::react::TelemetryTimePoint = facebook::react::TelemetryClock::time_point; using facebook::react::TextComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextLayoutManagerExtended = facebook::react::detail::TextLayoutManagerExtended; using facebook::react::TextMeasureCache = facebook::react::SimpleThreadSafeCache; @@ -7502,9 +7505,20 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textTransform; public std::optional baseWritingDirection; public std::string fontFamily; + public std::vector textEffects; public void apply(facebook::react::TextAttributes textAttributes); } +class facebook::react::TextEffectProps : public facebook::react::Props { + public TextEffectProps() = default; + public TextEffectProps(const facebook::react::PropsParserContext& context, const facebook::react::TextEffectProps& sourceProps, const facebook::react::RawProps& rawProps); + public folly::dynamic effectProps; + public std::string effectName; +} + +class facebook::react::TextEffectShadowNode : public facebook::react::ConcreteShadowNode { +} + class facebook::react::TextInputComponentDescriptor : public facebook::react::ConcreteComponentDescriptor { protected virtual void adopt(facebook::react::ShadowNode& shadowNode) const override; public TextInputComponentDescriptor(const facebook::react::ComponentDescriptorParameters& parameters); @@ -10302,6 +10316,12 @@ struct facebook::react::Task : public facebook::jsi::NativeState { public Task(facebook::react::SchedulerPriority priority, facebook::react::RawCallback&& callback, facebook::react::HighResTimeStamp expirationTime); } +struct facebook::react::TextEffectInfo { + public bool operator==(const facebook::react::TextEffectInfo&) const = default; + public folly::dynamic props; + public std::string name; +} + struct facebook::react::TextLayoutContext { public bool operator==(const facebook::react::TextLayoutContext& rhs) const = default; public facebook::react::Float pointScaleFactor; @@ -16031,6 +16051,10 @@ struct std::hash { public size_t operator()(const facebook::react::TextAttributes& textAttributes) const; } +struct std::hash { + public size_t operator()(const facebook::react::TextEffectInfo& info) const; +} + struct std::hash { public size_t operator()(const facebook::react::TextMeasureCacheKey& key) const; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index bcfb19940fae..3914705ee83b 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -153,6 +153,7 @@ const char facebook::react::RuntimeSchedulerKey[]; const char facebook::react::SafeAreaViewComponentName[]; const char facebook::react::ScrollViewComponentName[]; const char facebook::react::TextComponentName[]; +const char facebook::react::TextEffectComponentName[]; const char facebook::react::UnimplementedViewComponentName[]; const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; @@ -315,6 +316,8 @@ using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; using facebook::react::TelemetryTimePoint = facebook::react::TelemetryClock::time_point; using facebook::react::TextComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextLayoutManagerExtended = facebook::react::detail::TextLayoutManagerExtended; using facebook::react::TextMeasureCache = facebook::react::SimpleThreadSafeCache; @@ -3465,9 +3468,20 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textTransform; public std::optional baseWritingDirection; public std::string fontFamily; + public std::vector textEffects; public void apply(facebook::react::TextAttributes textAttributes); } +class facebook::react::TextEffectProps : public facebook::react::Props { + public TextEffectProps() = default; + public TextEffectProps(const facebook::react::PropsParserContext& context, const facebook::react::TextEffectProps& sourceProps, const facebook::react::RawProps& rawProps); + public folly::dynamic effectProps; + public std::string effectName; +} + +class facebook::react::TextEffectShadowNode : public facebook::react::ConcreteShadowNode { +} + class facebook::react::TextInputEventEmitter : public facebook::react::BaseViewEventEmitter { public void onBlur(const facebook::react::TextInputEventEmitter::Metrics& textInputMetrics) const; public void onChange(const facebook::react::TextInputEventEmitter::Metrics& textInputMetrics) const; @@ -6049,6 +6063,12 @@ struct facebook::react::Task : public facebook::jsi::NativeState { public Task(facebook::react::SchedulerPriority priority, facebook::react::RawCallback&& callback, facebook::react::HighResTimeStamp expirationTime); } +struct facebook::react::TextEffectInfo { + public bool operator==(const facebook::react::TextEffectInfo&) const = default; + public folly::dynamic props; + public std::string name; +} + struct facebook::react::TextLayoutContext { public Float pointScaleFactor; public bool operator==(const facebook::react::TextLayoutContext& rhs) const = default; @@ -10579,6 +10599,10 @@ struct std::hash { public size_t operator()(const facebook::react::TextAttributes& textAttributes) const; } +struct std::hash { + public size_t operator()(const facebook::react::TextEffectInfo& info) const; +} + struct std::hash { public size_t operator()(const facebook::react::TextMeasureCacheKey& key) const; } diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 53c14ca18d22..c294e79f1ee6 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -153,6 +153,7 @@ const char facebook::react::RuntimeSchedulerKey[]; const char facebook::react::SafeAreaViewComponentName[]; const char facebook::react::ScrollViewComponentName[]; const char facebook::react::TextComponentName[]; +const char facebook::react::TextEffectComponentName[]; const char facebook::react::UnimplementedViewComponentName[]; const char facebook::react::ViewComponentName[]; const facebook::react::EventTag facebook::react::EMPTY_EVENT_TAG; @@ -315,6 +316,8 @@ using facebook::react::TelemetryClock = std::chrono::steady_clock; using facebook::react::TelemetryDuration = std::chrono::nanoseconds; using facebook::react::TelemetryTimePoint = facebook::react::TelemetryClock::time_point; using facebook::react::TextComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectComponentDescriptor = facebook::react::ConcreteComponentDescriptor; +using facebook::react::TextEffectEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextEventEmitter = facebook::react::TouchEventEmitter; using facebook::react::TextLayoutManagerExtended = facebook::react::detail::TextLayoutManagerExtended; using facebook::react::TextMeasureCache = facebook::react::SimpleThreadSafeCache; @@ -3456,9 +3459,20 @@ class facebook::react::TextAttributes : public facebook::react::DebugStringConve public std::optional textTransform; public std::optional baseWritingDirection; public std::string fontFamily; + public std::vector textEffects; public void apply(facebook::react::TextAttributes textAttributes); } +class facebook::react::TextEffectProps : public facebook::react::Props { + public TextEffectProps() = default; + public TextEffectProps(const facebook::react::PropsParserContext& context, const facebook::react::TextEffectProps& sourceProps, const facebook::react::RawProps& rawProps); + public folly::dynamic effectProps; + public std::string effectName; +} + +class facebook::react::TextEffectShadowNode : public facebook::react::ConcreteShadowNode { +} + class facebook::react::TextInputEventEmitter : public facebook::react::BaseViewEventEmitter { public void onBlur(const facebook::react::TextInputEventEmitter::Metrics& textInputMetrics) const; public void onChange(const facebook::react::TextInputEventEmitter::Metrics& textInputMetrics) const; @@ -6040,6 +6054,12 @@ struct facebook::react::Task : public facebook::jsi::NativeState { public Task(facebook::react::SchedulerPriority priority, facebook::react::RawCallback&& callback, facebook::react::HighResTimeStamp expirationTime); } +struct facebook::react::TextEffectInfo { + public bool operator==(const facebook::react::TextEffectInfo&) const = default; + public folly::dynamic props; + public std::string name; +} + struct facebook::react::TextLayoutContext { public Float pointScaleFactor; public bool operator==(const facebook::react::TextLayoutContext& rhs) const = default; @@ -10570,6 +10590,10 @@ struct std::hash { public size_t operator()(const facebook::react::TextAttributes& textAttributes) const; } +struct std::hash { + public size_t operator()(const facebook::react::TextEffectInfo& info) const; +} + struct std::hash { public size_t operator()(const facebook::react::TextMeasureCacheKey& key) const; }