From 0d1811ea0850b7b15f2be97a70221bdefcabff24 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 26 Nov 2025 10:38:01 +0100 Subject: [PATCH 01/18] feat: add clip-path support for Android --- .../uimanager/BackgroundStyleApplicator.kt | 73 +++++ .../react/uimanager/BaseViewManager.java | 6 + .../uimanager/BaseViewManagerDelegate.kt | 2 + .../com/facebook/react/uimanager/ViewProps.kt | 1 + .../react/uimanager/style/ClipPath.kt | 255 ++++++++++++++++++ .../react/uimanager/style/ClipPathUtils.kt | 216 +++++++++++++++ .../react/views/image/ReactImageView.kt | 8 + .../views/text/PreparedLayoutTextView.kt | 7 +- .../react/views/text/ReactTextView.java | 12 + .../react/views/textinput/ReactEditText.kt | 9 + .../react/views/view/GeometryBoxUtil.kt | 178 ++++++++++++ .../react/views/view/ReactViewGroup.kt | 8 + .../react/views/view/ReactViewManager.kt | 87 ++++++ .../main/res/views/uimanager/values/ids.xml | 3 + 14 files changed, 863 insertions(+), 2 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt create mode 100644 packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/GeometryBoxUtil.kt diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 8e1fd2ef02bf..3916db8dff83 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -20,7 +20,9 @@ import android.os.Build import android.view.View import android.widget.ImageView import androidx.annotation.ColorInt +import com.facebook.react.R import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap import com.facebook.react.common.annotations.UnstableReactNativeAPI import com.facebook.react.uimanager.PixelUtil.dpToPx import com.facebook.react.uimanager.PixelUtil.pxToDp @@ -42,8 +44,12 @@ import com.facebook.react.uimanager.style.BorderRadiusProp import com.facebook.react.uimanager.style.BorderRadiusStyle import com.facebook.react.uimanager.style.BorderStyle import com.facebook.react.uimanager.style.BoxShadow +import com.facebook.react.uimanager.style.ClipPath +import com.facebook.react.uimanager.style.ClipPathUtils import com.facebook.react.uimanager.style.LogicalEdge import com.facebook.react.uimanager.style.OutlineStyle +import com.facebook.react.views.view.GeometryBoxUtil +import com.facebook.react.views.view.GeometryBoxUtil.getGeometryBoxBounds /** * Utility object responsible for applying backgrounds, borders, and related visual effects to @@ -440,6 +446,73 @@ public object BackgroundStyleApplicator { BackgroundStyleApplicator.setBoxShadow(view, shadowStyles) } + @JvmStatic + public fun setClipPath(view: View, clipPathMap: ReadableMap?) { + if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { + return + } + + val clipPath = ClipPath.parse(clipPathMap) + view.setTag(R.id.clip_path, clipPath) + view.invalidate() + } + + @JvmStatic + public fun applyClipPathIfPresent(view: View, canvas: Canvas) { + val clipPath = view.getTag(R.id.clip_path) as? ClipPath ?: return + val bounds = getGeometryBoxBounds(view, clipPath.geometryBox, getComputedBorderInsets(view)) + val drawingRect = Rect() + view.getDrawingRect(drawingRect) + + val path: Path? = if (clipPath.shape != null) { + ClipPathUtils.createPathFromBasicShape(clipPath.shape, bounds) + } else if (clipPath.geometryBox != null) { + val composite = getCompositeBackgroundDrawable(view) + val borderRadius = composite?.borderRadius + val computedBorderInsets = + composite?.borderInsets?.resolve(composite.layoutDirection, view.context) + + if (borderRadius != null) { + val adjustedBorderRadius = GeometryBoxUtil.adjustBorderRadiusForGeometryBox( + clipPath.geometryBox, + borderRadius.resolve( + composite.layoutDirection, + view.context, + PixelUtil.toDIPFromPixel(drawingRect.width().toFloat()), + PixelUtil.toDIPFromPixel(drawingRect.height().toFloat()) + ), + computedBorderInsets, + view + ) + + if (adjustedBorderRadius != null) { + ClipPathUtils.createRoundedRectPath(bounds, adjustedBorderRadius) + } else { + null + } + } else { + null + } + } else { + null + } + + if (path != null) { + canvas.clipPath(path) + } else { + canvas.clipRect(bounds) + } + } + + @JvmStatic + public fun getComputedBorderInsets(view: View): RectF? { + val composite = getCompositeBackgroundDrawable(view) + if (composite == null) { + return null + } + return composite.borderInsets?.resolve(composite.layoutDirection, view.context) + } + /** * Sets a feedback underlay drawable for the view. * diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index dd4198e4e50f..1264dcee21c1 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -126,6 +126,7 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) { view.setTag(R.id.use_hardware_layer, null); view.setTag(R.id.filter, null); view.setTag(R.id.mix_blend_mode, null); + view.setTag(R.id.clip_path, null); LayerEffectsHelper.apply(view, null, null); // setShadowColor @@ -875,6 +876,11 @@ public void setBoxShadow(T view, @Nullable ReadableArray shadows) { BackgroundStyleApplicator.setBoxShadow(view, shadows); } + @ReactProp(name = ViewProps.CLIP_PATH, customType = "ClipPath") + public void setClipPath(T view, @Nullable ReadableMap clipPath) { + BackgroundStyleApplicator.setClipPath(view, clipPath); + } + private void logUnsupportedPropertyWarning(String propName) { FLog.w(ReactConstants.TAG, "%s doesn't support property '%s'", getName(), propName); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt index d2164e77b192..8d2dcec6fe64 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt @@ -82,6 +82,8 @@ public abstract class BaseViewManagerDelegate< ViewProps.BOX_SHADOW -> mViewManager.setBoxShadow(view, value as ReadableArray?) + ViewProps.CLIP_PATH -> mViewManager.setClipPath(view, value as ReadableMap?) + ViewProps.ELEVATION -> mViewManager.setElevation(view, (value as Double?)?.toFloat() ?: 0.0f) ViewProps.FILTER -> mViewManager.setFilter(view, value as ReadableArray?) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt index 8aeb06848370..5b4478c1813b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt @@ -146,6 +146,7 @@ public object ViewProps { public const val BORDER_START_COLOR: String = "borderStartColor" public const val BORDER_END_COLOR: String = "borderEndColor" public const val BOX_SHADOW: String = "boxShadow" + public const val CLIP_PATH: String = "clipPath" public const val FILTER: String = "filter" public const val MIX_BLEND_MODE: String = "mixBlendMode" public const val OUTLINE_COLOR: String = "outlineColor" diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt new file mode 100644 index 000000000000..7fcbc5a30fb4 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt @@ -0,0 +1,255 @@ +/* + * 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.uimanager.style + +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.ReadableType +import com.facebook.react.uimanager.LengthPercentage + +private fun getOptionalLengthPercentage(map: ReadableMap, key: String): LengthPercentage? { + return if (map.hasKey(key)) { + LengthPercentage.setFromDynamic(map.getDynamic(key)) + } else { + null + } +} + +public data class CircleShape( + val r: LengthPercentage? = null, + val cx: LengthPercentage? = null, + val cy: LengthPercentage? = null, +) { + public companion object { + public fun parse(map: ReadableMap): CircleShape? { + val r = getOptionalLengthPercentage(map, "r") + val cx = getOptionalLengthPercentage(map, "cx") + val cy = getOptionalLengthPercentage(map, "cy") + return CircleShape(r, cx, cy) + } + } +} + +public data class EllipseShape( + val rx: LengthPercentage? = null, + val ry: LengthPercentage? = null, + val cx: LengthPercentage? = null, + val cy: LengthPercentage? = null, +) { + public companion object { + public fun parse(map: ReadableMap): EllipseShape? { + val rx = getOptionalLengthPercentage(map, "rx") + val ry = getOptionalLengthPercentage(map, "ry") + val cx = getOptionalLengthPercentage(map, "cx") + val cy = getOptionalLengthPercentage(map, "cy") + return EllipseShape(rx, ry, cx, cy) + } + } +} + +public data class InsetShape( + val top: LengthPercentage, + val right: LengthPercentage, + val bottom: LengthPercentage, + val left: LengthPercentage, + val borderRadius: LengthPercentage? = null, +) { + public companion object { + public fun parse(map: ReadableMap): InsetShape? { + val top = getOptionalLengthPercentage(map, "top") ?: return null + val right = getOptionalLengthPercentage(map, "right") ?: return null + val bottom = getOptionalLengthPercentage(map, "bottom") ?: return null + val left = getOptionalLengthPercentage(map, "left") ?: return null + val borderRadius = getOptionalLengthPercentage(map, "borderRadius") + return InsetShape(top, right, bottom, left, borderRadius) + } + } +} + +public enum class FillRule { + NonZero, + EvenOdd; + + public companion object { + public fun fromString(value: String): FillRule { + return when (value.lowercase()) { + "nonzero" -> NonZero + "evenodd" -> EvenOdd + else -> NonZero + } + } + } +} + +public data class PolygonShape( + val points: List>, + val fillRule: FillRule? = null, +) { + public companion object { + public fun parse(map: ReadableMap): PolygonShape? { + if (!map.hasKey("points")) return null + val pointsArray = map.getArray("points") ?: return null + val points = mutableListOf>() + + for (i in 0 until pointsArray.size()) { + val pointMap = pointsArray.getMap(i) ?: continue + val x = getOptionalLengthPercentage(pointMap, "x") ?: continue + val y = getOptionalLengthPercentage(pointMap, "y") ?: continue + points.add(Pair(x, y)) + } + + val fillRule = + if (map.hasKey("fillRule")) { + FillRule.fromString(map.getString("fillRule") ?: "nonzero") + } else { + null + } + + return PolygonShape(points, fillRule) + } + } +} + +public data class RectShape( + val top: LengthPercentage, + val right: LengthPercentage, + val bottom: LengthPercentage, + val left: LengthPercentage, + val borderRadius: LengthPercentage? = null, +) { + public companion object { + public fun parse(map: ReadableMap): RectShape? { + val top = getOptionalLengthPercentage(map, "top") ?: return null + val right = getOptionalLengthPercentage(map, "right") ?: return null + val bottom = getOptionalLengthPercentage(map, "bottom") ?: return null + val left = getOptionalLengthPercentage(map, "left") ?: return null + val borderRadius = getOptionalLengthPercentage(map, "borderRadius") + return RectShape(top, right, bottom, left, borderRadius) + } + } +} + +public data class XywhShape( + val x: LengthPercentage, + val y: LengthPercentage, + val width: LengthPercentage, + val height: LengthPercentage, + val borderRadius: LengthPercentage? = null, +) { + public companion object { + public fun parse(map: ReadableMap): XywhShape? { + val x = getOptionalLengthPercentage(map, "x") ?: return null + val y = getOptionalLengthPercentage(map, "y") ?: return null + val width = getOptionalLengthPercentage(map, "width") ?: return null + val height = getOptionalLengthPercentage(map, "height") ?: return null + val borderRadius = getOptionalLengthPercentage(map, "borderRadius") + return XywhShape(x, y, width, height, borderRadius) + } + } +} + +public sealed class BasicShape { + public data class Circle(val shape: CircleShape) : BasicShape() + + public data class Ellipse(val shape: EllipseShape) : BasicShape() + + public data class Inset(val shape: InsetShape) : BasicShape() + + public data class Polygon(val shape: PolygonShape) : BasicShape() + + public data class Rect(val shape: RectShape) : BasicShape() + + public data class Xywh(val shape: XywhShape) : BasicShape() + + public companion object { + public fun parse(map: ReadableMap): BasicShape? { + if (!map.hasKey("type")) return null + val type = map.getString("type") ?: return null + + return when (type.lowercase()) { + "circle" -> { + val circle = CircleShape.parse(map) ?: return null + Circle(circle) + } + "ellipse" -> { + val ellipse = EllipseShape.parse(map) ?: return null + Ellipse(ellipse) + } + "inset" -> { + val inset = InsetShape.parse(map) ?: return null + Inset(inset) + } + "polygon" -> { + val polygon = PolygonShape.parse(map) ?: return null + Polygon(polygon) + } + "rect" -> { + val rect = RectShape.parse(map) ?: return null + Rect(rect) + } + "xywh" -> { + val xywh = XywhShape.parse(map) ?: return null + Xywh(xywh) + } + else -> null + } + } + } +} + +public enum class GeometryBox { + MarginBox, + BorderBox, + ContentBox, + PaddingBox, + FillBox, + StrokeBox, + ViewBox; + + public companion object { + public fun fromString(value: String): GeometryBox { + return when (value.lowercase()) { + "margin-box" -> MarginBox + "border-box" -> BorderBox + "content-box" -> ContentBox + "padding-box" -> PaddingBox + "fill-box" -> FillBox + "stroke-box" -> StrokeBox + "view-box" -> ViewBox + else -> BorderBox + } + } + } +} + +public data class ClipPath( + val shape: BasicShape? = null, + val geometryBox: GeometryBox? = null, +) { + public companion object { + public fun parse(map: ReadableMap?): ClipPath? { + if (map == null) return null + + val shape = + if (map.hasKey("shape") && map.getType("shape") == ReadableType.Map) { + val shapeMap = map.getMap("shape") + if (shapeMap != null) BasicShape.parse(shapeMap) else null + } else { + null + } + + val geometryBox = + if (map.hasKey("geometryBox")) { + GeometryBox.fromString(map.getString("geometryBox") ?: "border-box") + } else { + null + } + + return ClipPath(shape, geometryBox) + } + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt new file mode 100644 index 000000000000..2867a37503b1 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt @@ -0,0 +1,216 @@ +/* + * 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.uimanager.style + +import android.graphics.Path +import android.graphics.Path.FillType +import android.graphics.RectF +import com.facebook.react.uimanager.LengthPercentage +import com.facebook.react.uimanager.LengthPercentageType +import com.facebook.react.uimanager.PixelUtil + +public object ClipPathUtils { + + private fun resolveLengthPercentage(lengthPercentage: LengthPercentage, referenceDimension: Float): Float { + return when (lengthPercentage.type) { + LengthPercentageType.POINT -> PixelUtil.toPixelFromDIP(lengthPercentage.resolve(1f)) + LengthPercentageType.PERCENT -> lengthPercentage.resolve(referenceDimension) + } + } + + private fun addRectWithOptionalBorderRadius( + path: Path, + rect: RectF, + borderRadius: LengthPercentage?, + ) { + if (borderRadius != null) { + val referenceDimension = minOf(rect.width(), rect.height()) + val radius = resolveLengthPercentage(borderRadius, referenceDimension) + path.addRoundRect(rect, radius, radius, Path.Direction.CW) + } else { + path.addRect(rect, Path.Direction.CW) + } + } + + public fun createPathFromBasicShape(basicShape: BasicShape, bounds: RectF): Path? { + return when (basicShape) { + is BasicShape.Circle -> createCirclePath(basicShape.shape, bounds) + is BasicShape.Ellipse -> createEllipsePath(basicShape.shape, bounds) + is BasicShape.Inset -> createInsetPath(basicShape.shape, bounds) + is BasicShape.Polygon -> createPolygonPath(basicShape.shape, bounds) + is BasicShape.Rect -> createRectPath(basicShape.shape, bounds) + is BasicShape.Xywh -> createXywhPath(basicShape.shape, bounds) + } + } + + private fun createCirclePath(circle: CircleShape, bounds: RectF): Path { + val path = Path() + + // Resolve radius (use smaller dimension as reference for percentages, matching CSS closest-side) + // Default to 50% of closest-side if radius is not specified + val referenceDimension = minOf(bounds.width(), bounds.height()) + val radius = if (circle.r != null) { + resolveLengthPercentage(circle.r, referenceDimension) + } else { + // Default to 50% of closest-side (min(width, height) / 2) + referenceDimension / 2.0f + } + + // Resolve center (default to center of bounds) + val cx = + if (circle.cx != null) { + bounds.left + resolveLengthPercentage(circle.cx, bounds.width()) + } else { + bounds.centerX() + } + + val cy = + if (circle.cy != null) { + bounds.top + resolveLengthPercentage(circle.cy, bounds.height()) + } else { + bounds.centerY() + } + + path.addCircle(cx, cy, radius, Path.Direction.CW) + return path + } + + private fun createEllipsePath(ellipse: EllipseShape, bounds: RectF): Path { + val path = Path() + + // Resolve radii (default to 50% if not specified) + val rx = if (ellipse.rx != null) { + resolveLengthPercentage(ellipse.rx, bounds.width()) + } else { + bounds.width() / 2.0f + } + val ry = if (ellipse.ry != null) { + resolveLengthPercentage(ellipse.ry, bounds.height()) + } else { + bounds.height() / 2.0f + } + + // Resolve center (default to center of bounds) + val cx = + if (ellipse.cx != null) { + bounds.left + resolveLengthPercentage(ellipse.cx, bounds.width()) + } else { + bounds.centerX() + } + + val cy = + if (ellipse.cy != null) { + bounds.top + resolveLengthPercentage(ellipse.cy, bounds.height()) + } else { + bounds.centerY() + } + + val oval = RectF(cx - rx, cy - ry, cx + rx, cy + ry) + path.addOval(oval, Path.Direction.CW) + return path + } + + private fun createInsetPath(inset: InsetShape, bounds: RectF): Path? { + val path = Path() + + val top = bounds.top + resolveLengthPercentage(inset.top, bounds.height()) + val right = bounds.right - resolveLengthPercentage(inset.right, bounds.width()) + val bottom = bounds.bottom - resolveLengthPercentage(inset.bottom, bounds.height()) + val left = bounds.left + resolveLengthPercentage(inset.left, bounds.width()) + + val rect = RectF(left, top, right, bottom) + if (rect.width() < 0f || rect.height() < 0f) { + return null + } + + addRectWithOptionalBorderRadius(path, rect, inset.borderRadius) + return path + } + + private fun createPolygonPath(polygon: PolygonShape, bounds: RectF): Path? { + val path = Path() + + if (polygon.points.isEmpty()) { + return null + } + + when (polygon.fillRule) { + FillRule.EvenOdd -> path.fillType = FillType.EVEN_ODD + FillRule.NonZero, + null -> path.fillType = FillType.WINDING + } + + val firstPoint = polygon.points[0] + val firstX = bounds.left + resolveLengthPercentage(firstPoint.first, bounds.width()) + val firstY = bounds.top + resolveLengthPercentage(firstPoint.second, bounds.height()) + path.moveTo(firstX, firstY) + + for (i in 1 until polygon.points.size) { + val point = polygon.points[i] + val x = bounds.left + resolveLengthPercentage(point.first, bounds.width()) + val y = bounds.top + resolveLengthPercentage(point.second, bounds.height()) + path.lineTo(x, y) + } + + path.close() + return path + } + + private fun createRectPath(rect: RectShape, bounds: RectF): Path? { + val path = Path() + + val top = bounds.top + resolveLengthPercentage(rect.top, bounds.height()) + val right = bounds.left + resolveLengthPercentage(rect.right, bounds.width()) + val bottom = bounds.top + resolveLengthPercentage(rect.bottom, bounds.height()) + val left = bounds.left + resolveLengthPercentage(rect.left, bounds.width()) + + val rectF = RectF(left, top, right, bottom) + if (rectF.width() < 0f || rectF.height() < 0f) { + return null + } + + addRectWithOptionalBorderRadius(path, rectF, rect.borderRadius) + return path + } + + private fun createXywhPath(xywh: XywhShape, bounds: RectF): Path? { + val path = Path() + + val x = bounds.left + resolveLengthPercentage(xywh.x, bounds.width()) + val y = bounds.top + resolveLengthPercentage(xywh.y, bounds.height()) + val width = resolveLengthPercentage(xywh.width, bounds.width()) + val height = resolveLengthPercentage(xywh.height, bounds.height()) + + val rect = RectF(x, y, x + width, y + height) + if (rect.width() < 0f || rect.height() < 0f) { + return null + } + + addRectWithOptionalBorderRadius(path, rect, xywh.borderRadius) + return path + } + + public fun createRoundedRectPath(bounds: RectF, borderRadius: ComputedBorderRadius): Path { + val path = Path() + + val topLeftRadii = borderRadius.topLeft + val topRightRadii = borderRadius.topRight + val bottomRightRadii = borderRadius.bottomRight + val bottomLeftRadii = borderRadius.bottomLeft + + val radii = floatArrayOf( + topLeftRadii.horizontal, topLeftRadii.vertical, + topRightRadii.horizontal, topRightRadii.vertical, + bottomRightRadii.horizontal, bottomRightRadii.vertical, + bottomLeftRadii.horizontal, bottomLeftRadii.vertical + ) + + path.addRoundRect(bounds, radii, Path.Direction.CW) + return path + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 252681d7b8d0..606ab0e7a546 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -22,6 +22,7 @@ import android.graphics.Shader.TileMode import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import android.net.Uri +import androidx.core.graphics.withSave import com.facebook.common.references.CloseableReference import com.facebook.common.util.UriUtil import com.facebook.drawee.backends.pipeline.Fresco @@ -368,6 +369,13 @@ public class ReactImageView( // or outline which may draw outside of bounds. public override fun hasOverlappingRendering(): Boolean = false + public override fun draw(canvas: Canvas) { + canvas.withSave { + BackgroundStyleApplicator.applyClipPathIfPresent(this@ReactImageView, this) + super.draw(this) + } + } + public override fun onDraw(canvas: Canvas) { BackgroundStyleApplicator.clipToPaddingBoxWithAntiAliasing(this, canvas) { try { 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 4202be214106..79cc777f0a64 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 @@ -24,6 +24,7 @@ import android.view.ViewParent import androidx.annotation.ColorInt import androidx.annotation.DoNotInline import androidx.annotation.RequiresApi +import androidx.core.graphics.withSave import androidx.core.view.ViewCompat import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.common.annotations.UnstableReactNativeAPI @@ -122,8 +123,10 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re if (overflow != Overflow.VISIBLE) { BackgroundStyleApplicator.clipToPaddingBox(this, canvas) } - - super.onDraw(canvas) + canvas.withSave { + BackgroundStyleApplicator.applyClipPathIfPresent(this@PreparedLayoutTextView, this) + super.onDraw(canvas) + } canvas.translate( paddingLeft.toFloat(), paddingTop.toFloat() + (preparedLayout?.verticalOffset ?: 0f), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index eed89a0aa568..f587d421a190 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -32,6 +32,7 @@ import androidx.customview.widget.ExploreByTouchHelper; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Nullsafe; +import com.facebook.react.R; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; @@ -45,6 +46,7 @@ import com.facebook.react.uimanager.ViewDefaults; import com.facebook.react.uimanager.style.BorderRadiusProp; import com.facebook.react.uimanager.style.BorderStyle; +import com.facebook.react.uimanager.style.ClipPath; import com.facebook.react.uimanager.style.LogicalEdge; import com.facebook.react.uimanager.style.Overflow; import com.facebook.react.util.AndroidVersion; @@ -255,6 +257,12 @@ protected void onDraw(Canvas canvas) { BackgroundStyleApplicator.clipToPaddingBox(this, canvas); } + ClipPath clipPath = (ClipPath) getTag(R.id.clip_path); + if (clipPath != null) { + canvas.save(); + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas); + } + if (spanned != null) { Layout layout = getLayout(); if (layout != null) { @@ -277,6 +285,10 @@ protected void onDraw(Canvas canvas) { } else { super.onDraw(canvas); } + + if (clipPath != null) { + canvas.restore(); + } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index 754f10026624..067f56d3eef4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -52,6 +52,7 @@ import com.facebook.react.common.assets.ReactFontManager import com.facebook.react.common.build.ReactBuildConfig import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags +import com.facebook.react.uimanager.BackgroundStyleApplicator import com.facebook.react.uimanager.BackgroundStyleApplicator.clipToPaddingBox import com.facebook.react.uimanager.BackgroundStyleApplicator.getBackgroundColor import com.facebook.react.uimanager.BackgroundStyleApplicator.getBorderColor @@ -93,6 +94,7 @@ import com.facebook.react.views.text.internal.span.ReactUnderlineSpan import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.max import kotlin.math.min +import androidx.core.graphics.withSave /** * A wrapper around the EditText that lets us better control what happens when an EditText gets @@ -1232,6 +1234,13 @@ public open class ReactEditText public constructor(context: Context) : AppCompat invalidate() } + public override fun draw(canvas: Canvas) { + canvas.withSave { + BackgroundStyleApplicator.applyClipPathIfPresent(this@ReactEditText, this) + super.draw(this) + } + } + public override fun onDraw(canvas: Canvas) { if (overflow != Overflow.VISIBLE) { clipToPaddingBox(this, canvas) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/GeometryBoxUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/GeometryBoxUtil.kt new file mode 100644 index 000000000000..4f18e31a9aeb --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/GeometryBoxUtil.kt @@ -0,0 +1,178 @@ +/* + * 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.view + +import android.graphics.RectF +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams +import com.facebook.react.uimanager.PixelUtil +import com.facebook.react.uimanager.PixelUtil.dpToPx +import com.facebook.react.uimanager.style.ComputedBorderRadius +import com.facebook.react.uimanager.style.CornerRadii +import com.facebook.react.uimanager.style.GeometryBox +import kotlin.math.max +import kotlin.math.roundToInt + +internal object GeometryBoxUtil { + + @JvmStatic + fun adjustBorderRadiusForGeometryBox( + geometryBox: GeometryBox?, + borderRadius: ComputedBorderRadius?, + computedBorderInsets: RectF?, + view: View + ): ComputedBorderRadius? { + if (borderRadius == null) { + return null + } + + val params = view.layoutParams as? MarginLayoutParams + + return when (geometryBox) { + GeometryBox.MarginBox -> { + // margin-box: extend border-radius by margin amount + val marginLeft = params?.leftMargin?.toFloat() ?: 0f + val marginTop = params?.topMargin?.toFloat() ?: 0f + val marginRight = params?.rightMargin?.toFloat() ?: 0f + val marginBottom = params?.bottomMargin?.toFloat() ?: 0f + + ComputedBorderRadius( + topLeft = CornerRadii( + horizontal = borderRadius.topLeft.horizontal + marginLeft, + vertical = borderRadius.topLeft.vertical + marginTop + ), + topRight = CornerRadii( + horizontal = borderRadius.topRight.horizontal + marginRight, + vertical = borderRadius.topRight.vertical + marginTop + ), + bottomLeft = CornerRadii( + horizontal = borderRadius.bottomLeft.horizontal + marginLeft, + vertical = borderRadius.bottomLeft.vertical + marginBottom + ), + bottomRight = CornerRadii( + horizontal = borderRadius.bottomRight.horizontal + marginRight, + vertical = borderRadius.bottomRight.vertical + marginBottom + ) + ) + } + + GeometryBox.BorderBox, null -> { + // border-box: use border-radius as-is (this is the reference) + ComputedBorderRadius( + topLeft = borderRadius.topLeft.toPixelFromDIP(), + topRight = borderRadius.topRight.toPixelFromDIP(), + bottomLeft = borderRadius.bottomLeft.toPixelFromDIP(), + bottomRight = borderRadius.bottomRight.toPixelFromDIP() + ) + } + + GeometryBox.PaddingBox -> { + // padding-box: reduce border-radius by border width + val borderLeft = computedBorderInsets?.left ?: 0f + val borderTop = computedBorderInsets?.top ?: 0f + val borderRight = computedBorderInsets?.right ?: 0f + val borderBottom = computedBorderInsets?.bottom ?: 0f + + ComputedBorderRadius( + topLeft = CornerRadii( + horizontal = max(0f, borderRadius.topLeft.horizontal - borderLeft).dpToPx(), + vertical = max(0f, borderRadius.topLeft.vertical - borderTop).dpToPx() + ), + topRight = CornerRadii( + horizontal = max(0f, borderRadius.topRight.horizontal - borderRight).dpToPx(), + vertical = max(0f, borderRadius.topRight.vertical - borderTop).dpToPx() + ), + bottomLeft = CornerRadii( + horizontal = max(0f, borderRadius.bottomLeft.horizontal - borderLeft).dpToPx(), + vertical = max(0f, borderRadius.bottomLeft.vertical - borderBottom).dpToPx() + ), + bottomRight = CornerRadii( + horizontal = max(0f, borderRadius.bottomRight.horizontal - borderRight).dpToPx(), + vertical = max(0f, borderRadius.bottomRight.vertical - borderBottom).dpToPx() + ) + ) + } + + GeometryBox.ContentBox -> { + // content-box: reduce border-radius by border width + padding + // padding already includes border width + val paddingLeft = PixelUtil.toDIPFromPixel(view.paddingLeft.toFloat()).roundToInt() + val paddingTop = PixelUtil.toDIPFromPixel(view.paddingTop.toFloat()).roundToInt() + val paddingRight = PixelUtil.toDIPFromPixel(view.paddingRight.toFloat()).roundToInt() + val paddingBottom = PixelUtil.toDIPFromPixel(view.paddingBottom.toFloat()).roundToInt() + + ComputedBorderRadius( + topLeft = CornerRadii( + horizontal = max(0f, borderRadius.topLeft.horizontal - paddingLeft).dpToPx(), + vertical = max(0f, borderRadius.topLeft.vertical - paddingTop).dpToPx() + ), + topRight = CornerRadii( + horizontal = max(0f, borderRadius.topRight.horizontal - paddingRight).dpToPx(), + vertical = max(0f, borderRadius.topRight.vertical - paddingTop).dpToPx() + ), + bottomLeft = CornerRadii( + horizontal = max(0f, borderRadius.bottomLeft.horizontal - paddingLeft).dpToPx(), + vertical = max(0f, borderRadius.bottomLeft.vertical - paddingBottom).dpToPx() + ), + bottomRight = CornerRadii( + horizontal = max(0f, borderRadius.bottomRight.horizontal - paddingRight).dpToPx(), + vertical = max(0f, borderRadius.bottomRight.vertical - paddingBottom).dpToPx() + ) + ) + } + + else -> borderRadius // StrokeBox, ViewBox, FillBox - use border-box as fallback + } + } + + @JvmStatic + fun getGeometryBoxBounds(view: View, geometryBox: GeometryBox?, computedBorderInsets: RectF?): RectF { + val bounds = RectF(0f, 0f, view.width.toFloat(), view.height.toFloat()) + val params = view.layoutParams as? MarginLayoutParams + val box = when (geometryBox) { + GeometryBox.ContentBox -> { + // ContentBox = BorderBox + padding + RectF( + bounds.left + view.paddingLeft, + bounds.top + view.paddingTop, + bounds.right - view.paddingRight, + bounds.bottom - view.paddingBottom + ) + } + + GeometryBox.PaddingBox -> { + // PaddingBox = BorderBox - border + RectF( + bounds.left + (computedBorderInsets?.left?.dpToPx() ?: 0f), + bounds.top + (computedBorderInsets?.top?.dpToPx() ?: 0f), + bounds.right - (computedBorderInsets?.right?.dpToPx() ?: 0f), + bounds.bottom - (computedBorderInsets?.bottom?.dpToPx() ?: 0f) + ) + } + + GeometryBox.MarginBox -> { + // MarginBox = BorderBox + margin + RectF( + bounds.left - (params?.leftMargin?.dpToPx() ?: 0f), + bounds.top - (params?.topMargin?.dpToPx() ?: 0f), + bounds.right + (params?.rightMargin?.dpToPx() ?: 0f), + bounds.bottom + (params?.bottomMargin?.dpToPx() ?: 0f) + ) + } + + GeometryBox.BorderBox, null -> { + // BorderBox = view bounds + bounds + } + + else -> bounds // StrokeBox, ViewBox - use border-box as fallback + } + return box + } +} diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index ae2e36ca1ed6..77190fdc6617 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -38,6 +38,7 @@ import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags import com.facebook.react.touch.OnInterceptTouchEventListener import com.facebook.react.touch.ReactHitSlopView import com.facebook.react.touch.ReactInterceptingViewGroup +import com.facebook.react.uimanager.BackgroundStyleApplicator import com.facebook.react.uimanager.BackgroundStyleApplicator.clipToPaddingBox import com.facebook.react.uimanager.BackgroundStyleApplicator.getPaddingBoxRect import com.facebook.react.uimanager.BackgroundStyleApplicator.setBackgroundColor @@ -220,6 +221,9 @@ public open class ReactViewGroup public constructor(context: Context?) : // Reset background, borders updateBackgroundDrawable(null) + // Reset clip path + setTag(R.id.clip_path, null) + resetPointerEvents() // In case a focus was attempted but the view never attached, reset to false @@ -901,9 +905,11 @@ public open class ReactViewGroup public constructor(context: Context?) : (height + -overflowInset.bottom).toFloat(), null, ) + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) super.draw(canvas) canvas.restore() } else { + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) super.draw(canvas) } } @@ -912,6 +918,8 @@ public open class ReactViewGroup public constructor(context: Context?) : if (_overflow != Overflow.VISIBLE || getTag(R.id.filter) != null) { clipToPaddingBox(this, canvas) } + + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) super.dispatchDraw(canvas) } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt index fc0629f9cf72..6dbc03edea1e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -9,6 +9,9 @@ package com.facebook.react.views.view import android.graphics.Rect import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import android.view.ViewGroup.MarginLayoutParams import com.facebook.common.logging.FLog import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.DynamicFromObject @@ -43,6 +46,22 @@ import com.facebook.react.uimanager.style.LogicalEdge @ReactModule(name = ReactViewManager.REACT_CLASS) public open class ReactViewManager : ReactClippingViewManager() { + private enum class MarginIndex { + ALL, + VERTICAL, + HORIZONTAL, + LEFT, + RIGHT, + TOP, + BOTTOM, + START, + END; + + companion object { + fun fromIndex(index: Int): MarginIndex? = values().getOrNull(index) + } + } + public companion object { public const val REACT_CLASS: String = ViewProps.VIEW_CLASS_NAME @@ -244,6 +263,13 @@ public open class ReactViewManager : ReactClippingViewManager() setBackgroundRepeat(view, backgroundRepeat) } + @ReactProp(name = ViewProps.CLIP_PATH, customType = "ClipPath") + public override fun setClipPath(view: ReactViewGroup, clipPath: ReadableMap?) { + if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) { + BackgroundStyleApplicator.setClipPath(view, clipPath) + } + } + @ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID) public open fun nextFocusDown(view: ReactViewGroup, viewId: Int) { view.nextFocusDownId = viewId @@ -363,6 +389,67 @@ public open class ReactViewManager : ReactClippingViewManager() view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing) } +@ReactPropGroup( + names = + [ + ViewProps.MARGIN, + ViewProps.MARGIN_VERTICAL, + ViewProps.MARGIN_HORIZONTAL, + ViewProps.MARGIN_LEFT, + ViewProps.MARGIN_RIGHT, + ViewProps.MARGIN_TOP, + ViewProps.MARGIN_BOTTOM, + ViewProps.MARGIN_START, + ViewProps.MARGIN_END + ], + defaultFloat = Float.NaN, + ) + public open fun setMargin(view: ReactViewGroup, index: Int, margin: Float) { + + val layoutParams = view.layoutParams as? MarginLayoutParams ?: MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + val leftMargin = layoutParams.leftMargin; + val topMargin = layoutParams.topMargin; + val rightMargin = layoutParams.rightMargin; + val bottomMargin = layoutParams.bottomMargin; + when (MarginIndex.fromIndex(index)) { + MarginIndex.ALL -> { + layoutParams.setMargins(margin.toInt(), margin.toInt(), margin.toInt(), margin.toInt()) + } + MarginIndex.VERTICAL -> { + layoutParams.setMargins(leftMargin, margin.toInt(), rightMargin, margin.toInt()) + } + MarginIndex.HORIZONTAL -> { + layoutParams.setMargins(margin.toInt(), topMargin, margin.toInt(), bottomMargin) + } + MarginIndex.LEFT -> { + layoutParams.setMargins(margin.toInt(), topMargin, rightMargin, bottomMargin) + } + MarginIndex.RIGHT -> { + layoutParams.setMargins(leftMargin, topMargin, margin.toInt(), bottomMargin) + } + MarginIndex.TOP -> { + layoutParams.setMargins(leftMargin, margin.toInt(), rightMargin, bottomMargin) + } + MarginIndex.BOTTOM -> { + layoutParams.setMargins(leftMargin, topMargin, rightMargin, margin.toInt()) + } + MarginIndex.START -> { + layoutParams.setMargins(margin.toInt(), topMargin, rightMargin, bottomMargin) + } + MarginIndex.END -> { + layoutParams.setMargins(leftMargin, topMargin, margin.toInt(), bottomMargin) + } + null -> { + // Unknown index, do nothing + } + } + view.layoutParams = layoutParams + } + + override fun setPadding(view: ReactViewGroup, left: Int, top: Int, right: Int, bottom: Int) { + view.setPadding(left, top, right, bottom) + } + @ReactPropGroup( names = [ diff --git a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index 0e51a358eb77..4e1032e879a5 100644 --- a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -82,4 +82,7 @@ + + + From 86e9a71bdede27e94e4dbd9bef86a110b55875d5 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 2 Dec 2025 13:21:53 +0100 Subject: [PATCH 02/18] refactor: change visibility to internal --- .../react/uimanager/style/ClipPath.kt | 26 +++++++++---------- .../react/uimanager/style/ClipPathUtils.kt | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt index 7fcbc5a30fb4..6e4a7371ca97 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt @@ -19,7 +19,7 @@ private fun getOptionalLengthPercentage(map: ReadableMap, key: String): LengthPe } } -public data class CircleShape( +internal data class CircleShape( val r: LengthPercentage? = null, val cx: LengthPercentage? = null, val cy: LengthPercentage? = null, @@ -34,7 +34,7 @@ public data class CircleShape( } } -public data class EllipseShape( +internal data class EllipseShape( val rx: LengthPercentage? = null, val ry: LengthPercentage? = null, val cx: LengthPercentage? = null, @@ -51,7 +51,7 @@ public data class EllipseShape( } } -public data class InsetShape( +internal data class InsetShape( val top: LengthPercentage, val right: LengthPercentage, val bottom: LengthPercentage, @@ -85,7 +85,7 @@ public enum class FillRule { } } -public data class PolygonShape( +internal data class PolygonShape( val points: List>, val fillRule: FillRule? = null, ) { @@ -114,7 +114,7 @@ public data class PolygonShape( } } -public data class RectShape( +internal data class RectShape( val top: LengthPercentage, val right: LengthPercentage, val bottom: LengthPercentage, @@ -133,7 +133,7 @@ public data class RectShape( } } -public data class XywhShape( +internal data class XywhShape( val x: LengthPercentage, val y: LengthPercentage, val width: LengthPercentage, @@ -153,17 +153,17 @@ public data class XywhShape( } public sealed class BasicShape { - public data class Circle(val shape: CircleShape) : BasicShape() + internal data class Circle(val shape: CircleShape) : BasicShape() - public data class Ellipse(val shape: EllipseShape) : BasicShape() + internal data class Ellipse(val shape: EllipseShape) : BasicShape() - public data class Inset(val shape: InsetShape) : BasicShape() + internal data class Inset(val shape: InsetShape) : BasicShape() - public data class Polygon(val shape: PolygonShape) : BasicShape() + internal data class Polygon(val shape: PolygonShape) : BasicShape() - public data class Rect(val shape: RectShape) : BasicShape() + internal data class Rect(val shape: RectShape) : BasicShape() - public data class Xywh(val shape: XywhShape) : BasicShape() + internal data class Xywh(val shape: XywhShape) : BasicShape() public companion object { public fun parse(map: ReadableMap): BasicShape? { @@ -226,7 +226,7 @@ public enum class GeometryBox { } } -public data class ClipPath( +internal data class ClipPath( val shape: BasicShape? = null, val geometryBox: GeometryBox? = null, ) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt index 2867a37503b1..2655a2639757 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt @@ -14,7 +14,7 @@ import com.facebook.react.uimanager.LengthPercentage import com.facebook.react.uimanager.LengthPercentageType import com.facebook.react.uimanager.PixelUtil -public object ClipPathUtils { +internal object ClipPathUtils { private fun resolveLengthPercentage(lengthPercentage: LengthPercentage, referenceDimension: Float): Float { return when (lengthPercentage.type) { From 4aae1b26e13b26cbf19235e95202e745393c0422 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 2 Dec 2025 13:27:41 +0100 Subject: [PATCH 03/18] refactor: update drawing methods to always use draw() --- .../views/text/PreparedLayoutTextView.kt | 12 ++++++---- .../react/views/text/ReactTextView.java | 24 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) 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 79cc777f0a64..91c70faa4b25 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 @@ -118,15 +118,17 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re } } - @OptIn(UnstableReactNativeAPI::class) - override fun onDraw(canvas: Canvas) { - if (overflow != Overflow.VISIBLE) { - BackgroundStyleApplicator.clipToPaddingBox(this, canvas) - } + override fun draw(canvas: Canvas) { canvas.withSave { BackgroundStyleApplicator.applyClipPathIfPresent(this@PreparedLayoutTextView, this) super.onDraw(canvas) } + } + + override fun onDraw(canvas: Canvas) { + if (overflow != Overflow.VISIBLE) { + BackgroundStyleApplicator.clipToPaddingBox(this, canvas) + } canvas.translate( paddingLeft.toFloat(), paddingTop.toFloat() + (preparedLayout?.verticalOffset ?: 0f), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index f587d421a190..24d7ed274f7e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -227,6 +227,20 @@ protected void onLayout( // correctly in Fabric. } + + @Override + public void draw(Canvas canvas) { + ClipPath clipPath = (ClipPath) getTag(R.id.clip_path); + if (clipPath != null) { + canvas.save(); + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas); + } + super.draw(canvas); + if (clipPath != null) { + canvas.restore(); + } + } + @Override @SuppressWarnings("try") protected void onDraw(Canvas canvas) { @@ -257,12 +271,6 @@ protected void onDraw(Canvas canvas) { BackgroundStyleApplicator.clipToPaddingBox(this, canvas); } - ClipPath clipPath = (ClipPath) getTag(R.id.clip_path); - if (clipPath != null) { - canvas.save(); - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas); - } - if (spanned != null) { Layout layout = getLayout(); if (layout != null) { @@ -285,10 +293,6 @@ protected void onDraw(Canvas canvas) { } else { super.onDraw(canvas); } - - if (clipPath != null) { - canvas.restore(); - } } } From e40a4ed1f8784e855727db901e1eb2de2a5084c1 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 2 Dec 2025 13:21:25 +0100 Subject: [PATCH 04/18] refactor: move file to correct dir --- .../facebook/react/uimanager/BackgroundStyleApplicator.kt | 4 ++-- .../{views/view => uimanager/style}/GeometryBoxUtil.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename packages/react-native/ReactAndroid/src/main/java/com/facebook/react/{views/view => uimanager/style}/GeometryBoxUtil.kt (95%) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 3916db8dff83..6a9830ad95dc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -46,10 +46,10 @@ import com.facebook.react.uimanager.style.BorderStyle import com.facebook.react.uimanager.style.BoxShadow import com.facebook.react.uimanager.style.ClipPath import com.facebook.react.uimanager.style.ClipPathUtils +import com.facebook.react.uimanager.style.GeometryBoxUtil +import com.facebook.react.uimanager.style.GeometryBoxUtil.getGeometryBoxBounds import com.facebook.react.uimanager.style.LogicalEdge import com.facebook.react.uimanager.style.OutlineStyle -import com.facebook.react.views.view.GeometryBoxUtil -import com.facebook.react.views.view.GeometryBoxUtil.getGeometryBoxBounds /** * Utility object responsible for applying backgrounds, borders, and related visual effects to diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/GeometryBoxUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/GeometryBoxUtil.kt similarity index 95% rename from packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/GeometryBoxUtil.kt rename to packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/GeometryBoxUtil.kt index 4f18e31a9aeb..b0fd6c0509d7 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/GeometryBoxUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/GeometryBoxUtil.kt @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -package com.facebook.react.views.view +package com.facebook.react.uimanager.style import android.graphics.RectF import android.view.View @@ -109,15 +109,15 @@ internal object GeometryBoxUtil { ComputedBorderRadius( topLeft = CornerRadii( - horizontal = max(0f, borderRadius.topLeft.horizontal - paddingLeft).dpToPx(), + horizontal = max(0f, borderRadius.topLeft.horizontal - paddingLeft).dpToPx(), vertical = max(0f, borderRadius.topLeft.vertical - paddingTop).dpToPx() ), topRight = CornerRadii( - horizontal = max(0f, borderRadius.topRight.horizontal - paddingRight).dpToPx(), + horizontal = max(0f, borderRadius.topRight.horizontal - paddingRight).dpToPx(), vertical = max(0f, borderRadius.topRight.vertical - paddingTop).dpToPx() ), bottomLeft = CornerRadii( - horizontal = max(0f, borderRadius.bottomLeft.horizontal - paddingLeft).dpToPx(), + horizontal = max(0f, borderRadius.bottomLeft.horizontal - paddingLeft).dpToPx(), vertical = max(0f, borderRadius.bottomLeft.vertical - paddingBottom).dpToPx() ), bottomRight = CornerRadii( From 16f37309810e9def52b24f8bca758d5118585f06 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 2 Dec 2025 15:58:40 +0100 Subject: [PATCH 05/18] fix: offset bounds by drawingRect topLeft --- .../com/facebook/react/uimanager/BackgroundStyleApplicator.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 6a9830ad95dc..febf63c6c1cc 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -463,6 +463,7 @@ public object BackgroundStyleApplicator { val bounds = getGeometryBoxBounds(view, clipPath.geometryBox, getComputedBorderInsets(view)) val drawingRect = Rect() view.getDrawingRect(drawingRect) + bounds.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat()) val path: Path? = if (clipPath.shape != null) { ClipPathUtils.createPathFromBasicShape(clipPath.shape, bounds) From 158192051cc42f58e4cc0ed1346f73b4b3321760 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Tue, 2 Dec 2025 16:19:19 +0100 Subject: [PATCH 06/18] refactor: run canvas save/restore conditionally --- .../react/uimanager/BackgroundStyleApplicator.kt | 5 ++++- .../com/facebook/react/uimanager/style/ClipPath.kt | 2 +- .../facebook/react/views/image/ReactImageView.kt | 14 ++++++++++---- .../react/views/text/PreparedLayoutTextView.kt | 14 ++++++++++---- .../react/views/textinput/ReactEditText.kt | 14 ++++++++++---- .../facebook/react/views/view/ReactViewGroup.kt | 11 ++++++++++- 6 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index febf63c6c1cc..cb03cf3fa34e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -457,9 +457,12 @@ public object BackgroundStyleApplicator { view.invalidate() } + @JvmStatic + public fun getClipPath(view: View): ClipPath? = view.getTag(R.id.clip_path) as? ClipPath + @JvmStatic public fun applyClipPathIfPresent(view: View, canvas: Canvas) { - val clipPath = view.getTag(R.id.clip_path) as? ClipPath ?: return + val clipPath = getClipPath(view) ?: return val bounds = getGeometryBoxBounds(view, clipPath.geometryBox, getComputedBorderInsets(view)) val drawingRect = Rect() view.getDrawingRect(drawingRect) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt index 6e4a7371ca97..db2e91c1ed0b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPath.kt @@ -226,7 +226,7 @@ public enum class GeometryBox { } } -internal data class ClipPath( +public data class ClipPath( val shape: BasicShape? = null, val geometryBox: GeometryBox? = null, ) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 606ab0e7a546..989799c683c4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -22,7 +22,6 @@ import android.graphics.Shader.TileMode import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable import android.net.Uri -import androidx.core.graphics.withSave import com.facebook.common.references.CloseableReference import com.facebook.common.util.UriUtil import com.facebook.drawee.backends.pipeline.Fresco @@ -370,9 +369,16 @@ public class ReactImageView( public override fun hasOverlappingRendering(): Boolean = false public override fun draw(canvas: Canvas) { - canvas.withSave { - BackgroundStyleApplicator.applyClipPathIfPresent(this@ReactImageView, this) - super.draw(this) + val clipPath = BackgroundStyleApplicator.getClipPath(this) + if (clipPath != null) { + canvas.save() + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) + } + + super.onDraw(canvas) + + if (clipPath != null) { + canvas.restore() } } 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 91c70faa4b25..531ce260ef53 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 @@ -24,7 +24,6 @@ import android.view.ViewParent import androidx.annotation.ColorInt import androidx.annotation.DoNotInline import androidx.annotation.RequiresApi -import androidx.core.graphics.withSave import androidx.core.view.ViewCompat import com.facebook.proguard.annotations.DoNotStrip import com.facebook.react.common.annotations.UnstableReactNativeAPI @@ -119,9 +118,16 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re } override fun draw(canvas: Canvas) { - canvas.withSave { - BackgroundStyleApplicator.applyClipPathIfPresent(this@PreparedLayoutTextView, this) - super.onDraw(canvas) + val clipPath = BackgroundStyleApplicator.getClipPath(this) + if (clipPath != null) { + canvas.save() + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) + } + + super.draw(canvas) + + if (clipPath != null) { + canvas.restore() } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index 067f56d3eef4..99aca1ddbf67 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -94,7 +94,6 @@ import com.facebook.react.views.text.internal.span.ReactUnderlineSpan import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.max import kotlin.math.min -import androidx.core.graphics.withSave /** * A wrapper around the EditText that lets us better control what happens when an EditText gets @@ -1235,9 +1234,16 @@ public open class ReactEditText public constructor(context: Context) : AppCompat } public override fun draw(canvas: Canvas) { - canvas.withSave { - BackgroundStyleApplicator.applyClipPathIfPresent(this@ReactEditText, this) - super.draw(this) + val clipPath = BackgroundStyleApplicator.getClipPath(this) + if (clipPath != null) { + canvas.save() + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) + } + + super.draw(canvas) + + if (clipPath != null) { + canvas.restore() } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index 77190fdc6617..26cef7caa3a4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -919,8 +919,17 @@ public open class ReactViewGroup public constructor(context: Context?) : clipToPaddingBox(this, canvas) } - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) + val clipPath = BackgroundStyleApplicator.getClipPath(this) + if (clipPath != null) { + canvas.save() + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) + } + super.dispatchDraw(canvas) + + if (clipPath != null) { + canvas.restore() + } } override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean { From ea49d9c36d45bed19c273eb3eac023c4c92e719a Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Fri, 19 Dec 2025 13:02:15 +0100 Subject: [PATCH 07/18] feat(android): storing computed margin and padding information --- .../react/fabric/FabricUIManager.java | 10 + .../react/fabric/FabricUIManagerBinding.kt | 4 + .../uimanager/BackgroundStyleApplicator.kt | 25 +- .../react/uimanager/UIManagerHelper.kt | 32 +++ .../react/uimanager/style/ClipPathUtils.kt | 8 +- .../react/uimanager/style/GeometryBoxUtil.kt | 249 +++++++++--------- .../react/views/view/ReactViewManager.kt | 79 ------ .../react/fabric/ComputedBoxModelRegistry.cpp | 73 +++++ .../react/fabric/ComputedBoxModelRegistry.h | 84 ++++++ .../react/fabric/FabricMountingManager.cpp | 59 +++++ .../jni/react/fabric/FabricMountingManager.h | 11 + .../react/fabric/FabricUIManagerBinding.cpp | 49 ++++ .../jni/react/fabric/FabricUIManagerBinding.h | 6 + .../renderer/components/view/conversions.h | 13 + .../react/renderer/core/LayoutMetrics.cpp | 10 + .../react/renderer/core/LayoutMetrics.h | 9 +- 16 files changed, 504 insertions(+), 217 deletions(-) create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/fabric/ComputedBoxModelRegistry.cpp create mode 100644 packages/react-native/ReactAndroid/src/main/jni/react/fabric/ComputedBoxModelRegistry.h 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 0defdccbdb4a..dbb0afcbcfd0 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 @@ -1484,6 +1484,16 @@ public Map getPerformanceCounters() { return performanceCounters; } + @Nullable + public float[] getComputedMarginInsets(int surfaceId, int viewTag) { + return mBinding != null ? mBinding.getComputedMarginInsets(surfaceId, viewTag) : null; + } + + @Nullable + public float[] getComputedPaddingInsets(int surfaceId, int viewTag) { + return mBinding != null ? mBinding.getComputedPaddingInsets(surfaceId, viewTag) : null; + } + private class MountItemDispatchListener implements MountItemDispatcher.ItemDispatchListener { @UiThread @ThreadConfined(UI) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt index b1b3f12f50c2..e998f868a865 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt @@ -89,6 +89,10 @@ internal class FabricUIManagerBinding : HybridClassBase() { external fun mergeReactRevision(surfaceId: Int) + external fun getComputedMarginInsets(surfaceId: Int, viewTag: Int): FloatArray? + + external fun getComputedPaddingInsets(surfaceId: Int, viewTag: Int): FloatArray? + fun register( runtimeExecutor: RuntimeExecutor, runtimeScheduler: RuntimeScheduler, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index cb03cf3fa34e..6a4769628800 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -463,7 +463,18 @@ public object BackgroundStyleApplicator { @JvmStatic public fun applyClipPathIfPresent(view: View, canvas: Canvas) { val clipPath = getClipPath(view) ?: return - val bounds = getGeometryBoxBounds(view, clipPath.geometryBox, getComputedBorderInsets(view)) + val composite = getCompositeBackgroundDrawable(view) + val computedMarginInsets = UIManagerHelper.getComputedMarginInsets(view) + val computedPaddingInsets = UIManagerHelper.getComputedPaddingInsets(view) + val computedBorderInsets = + composite?.borderInsets?.resolve(composite.layoutDirection, view.context) + val bounds = getGeometryBoxBounds( + view, + clipPath.geometryBox, + computedMarginInsets, + computedPaddingInsets, + computedBorderInsets + ) val drawingRect = Rect() view.getDrawingRect(drawingRect) bounds.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat()) @@ -471,24 +482,16 @@ public object BackgroundStyleApplicator { val path: Path? = if (clipPath.shape != null) { ClipPathUtils.createPathFromBasicShape(clipPath.shape, bounds) } else if (clipPath.geometryBox != null) { - val composite = getCompositeBackgroundDrawable(view) val borderRadius = composite?.borderRadius - val computedBorderInsets = - composite?.borderInsets?.resolve(composite.layoutDirection, view.context) - if (borderRadius != null) { val adjustedBorderRadius = GeometryBoxUtil.adjustBorderRadiusForGeometryBox( - clipPath.geometryBox, - borderRadius.resolve( + view, clipPath.geometryBox, borderRadius.resolve( composite.layoutDirection, view.context, PixelUtil.toDIPFromPixel(drawingRect.width().toFloat()), PixelUtil.toDIPFromPixel(drawingRect.height().toFloat()) - ), - computedBorderInsets, - view + ), computedMarginInsets, computedPaddingInsets, computedBorderInsets ) - if (adjustedBorderRadius != null) { ClipPathUtils.createRoundedRectPath(bounds, adjustedBorderRadius) } else { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.kt index ae4fdf104e90..66445857e273 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.kt @@ -11,6 +11,7 @@ package com.facebook.react.uimanager import android.content.Context import android.content.ContextWrapper +import android.graphics.RectF import android.view.View import android.widget.EditText import androidx.core.view.ViewCompat @@ -163,4 +164,35 @@ public object UIManagerHelper { padding[PADDING_BOTTOM_INDEX] = PixelUtil.toDIPFromPixel(editText.paddingBottom.toFloat()) return padding } + + @JvmStatic + public fun getComputedMarginInsets(view: View): RectF? = + getComputedLayoutMetrics(view) { uiManager, surfaceId, viewTag -> + uiManager.getComputedMarginInsets(surfaceId, viewTag) + } + + @JvmStatic + public fun getComputedPaddingInsets(view: View): RectF? = + getComputedLayoutMetrics(view) { uiManager, surfaceId, viewTag -> + uiManager.getComputedPaddingInsets(surfaceId, viewTag) + } + + private inline fun getComputedLayoutMetrics( + view: View, + getter: (com.facebook.react.fabric.FabricUIManager, Int, Int) -> FloatArray? + ): RectF? { + val viewTag = view.id + if (viewTag == View.NO_ID || getUIManagerType(viewTag) != UIManagerType.FABRIC) { + return null + } + + val context = view.context as? ThemedReactContext ?: return null + val surfaceId = getSurfaceId(context) + val uiManager = getUIManager( + context.reactApplicationContext, + UIManagerType.FABRIC + ) as? com.facebook.react.fabric.FabricUIManager ?: return null + val array = getter(uiManager, surfaceId, viewTag) ?: return null + return RectF(array[0], array[1], array[2], array[3]) + } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt index 2655a2639757..299e3780d8ed 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/ClipPathUtils.kt @@ -198,10 +198,10 @@ internal object ClipPathUtils { public fun createRoundedRectPath(bounds: RectF, borderRadius: ComputedBorderRadius): Path { val path = Path() - val topLeftRadii = borderRadius.topLeft - val topRightRadii = borderRadius.topRight - val bottomRightRadii = borderRadius.bottomRight - val bottomLeftRadii = borderRadius.bottomLeft + val topLeftRadii = borderRadius.topLeft.toPixelFromDIP() + val topRightRadii = borderRadius.topRight.toPixelFromDIP() + val bottomRightRadii = borderRadius.bottomRight.toPixelFromDIP() + val bottomLeftRadii = borderRadius.bottomLeft.toPixelFromDIP() val radii = floatArrayOf( topLeftRadii.horizontal, topLeftRadii.vertical, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/GeometryBoxUtil.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/GeometryBoxUtil.kt index b0fd6c0509d7..bc75c0ddafff 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/GeometryBoxUtil.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/GeometryBoxUtil.kt @@ -9,170 +9,175 @@ package com.facebook.react.uimanager.style import android.graphics.RectF import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.MarginLayoutParams -import com.facebook.react.uimanager.PixelUtil import com.facebook.react.uimanager.PixelUtil.dpToPx -import com.facebook.react.uimanager.style.ComputedBorderRadius -import com.facebook.react.uimanager.style.CornerRadii -import com.facebook.react.uimanager.style.GeometryBox -import kotlin.math.max -import kotlin.math.roundToInt -internal object GeometryBoxUtil { +private inline val RectF?.leftOrZero: Float + get() = this?.left ?: 0f + +private inline val RectF?.topOrZero: Float + get() = this?.top ?: 0f + +private inline val RectF?.rightOrZero: Float + get() = this?.right ?: 0f + +private inline val RectF?.bottomOrZero: Float + get() = this?.bottom ?: 0f +internal object GeometryBoxUtil { @JvmStatic fun adjustBorderRadiusForGeometryBox( - geometryBox: GeometryBox?, - borderRadius: ComputedBorderRadius?, - computedBorderInsets: RectF?, - view: View + view: View, + geometryBox: GeometryBox?, + borderRadius: ComputedBorderRadius?, + marginInsets: RectF?, + paddingInsets: RectF?, + borderInsets: RectF? ): ComputedBorderRadius? { if (borderRadius == null) { return null } - val params = view.layoutParams as? MarginLayoutParams - return when (geometryBox) { GeometryBox.MarginBox -> { - // margin-box: extend border-radius by margin amount - val marginLeft = params?.leftMargin?.toFloat() ?: 0f - val marginTop = params?.topMargin?.toFloat() ?: 0f - val marginRight = params?.rightMargin?.toFloat() ?: 0f - val marginBottom = params?.bottomMargin?.toFloat() ?: 0f - - ComputedBorderRadius( - topLeft = CornerRadii( - horizontal = borderRadius.topLeft.horizontal + marginLeft, - vertical = borderRadius.topLeft.vertical + marginTop - ), - topRight = CornerRadii( - horizontal = borderRadius.topRight.horizontal + marginRight, - vertical = borderRadius.topRight.vertical + marginTop - ), - bottomLeft = CornerRadii( - horizontal = borderRadius.bottomLeft.horizontal + marginLeft, - vertical = borderRadius.bottomLeft.vertical + marginBottom - ), - bottomRight = CornerRadii( - horizontal = borderRadius.bottomRight.horizontal + marginRight, - vertical = borderRadius.bottomRight.vertical + marginBottom - ) - ) - } - - GeometryBox.BorderBox, null -> { - // border-box: use border-radius as-is (this is the reference) + // Margin-box: extend border-radius by margin ComputedBorderRadius( - topLeft = borderRadius.topLeft.toPixelFromDIP(), - topRight = borderRadius.topRight.toPixelFromDIP(), - bottomLeft = borderRadius.bottomLeft.toPixelFromDIP(), - bottomRight = borderRadius.bottomRight.toPixelFromDIP() + topLeft = CornerRadii( + horizontal = borderRadius.topLeft.horizontal + marginInsets.leftOrZero, + vertical = borderRadius.topLeft.vertical + marginInsets.topOrZero + ), + topRight = CornerRadii( + horizontal = borderRadius.topRight.horizontal + marginInsets.rightOrZero, + vertical = borderRadius.topRight.vertical + marginInsets.topOrZero + ), + bottomLeft = CornerRadii( + horizontal = borderRadius.bottomLeft.horizontal + marginInsets.leftOrZero, + vertical = borderRadius.bottomLeft.vertical + marginInsets.bottomOrZero + ), + bottomRight = CornerRadii( + horizontal = borderRadius.bottomRight.horizontal + marginInsets.rightOrZero, + vertical = borderRadius.bottomRight.vertical + marginInsets.bottomOrZero + ) ) } GeometryBox.PaddingBox -> { - // padding-box: reduce border-radius by border width - val borderLeft = computedBorderInsets?.left ?: 0f - val borderTop = computedBorderInsets?.top ?: 0f - val borderRight = computedBorderInsets?.right ?: 0f - val borderBottom = computedBorderInsets?.bottom ?: 0f - + // Padding-box: reduce border-radius by border width ComputedBorderRadius( - topLeft = CornerRadii( - horizontal = max(0f, borderRadius.topLeft.horizontal - borderLeft).dpToPx(), - vertical = max(0f, borderRadius.topLeft.vertical - borderTop).dpToPx() + topLeft = CornerRadii( + horizontal = (borderRadius.topLeft.horizontal - borderInsets.leftOrZero).coerceAtLeast( + 0f + ), + vertical = (borderRadius.topLeft.vertical - borderInsets.topOrZero.coerceAtLeast(0f)) + ), + topRight = CornerRadii( + horizontal = (borderRadius.topRight.horizontal - borderInsets.rightOrZero).coerceAtLeast( + 0f ), - topRight = CornerRadii( - horizontal = max(0f, borderRadius.topRight.horizontal - borderRight).dpToPx(), - vertical = max(0f, borderRadius.topRight.vertical - borderTop).dpToPx() + vertical = (borderRadius.topRight.vertical - borderInsets.topOrZero.coerceAtLeast(0f)) + ), + bottomLeft = CornerRadii( + horizontal = (borderRadius.bottomLeft.horizontal - borderInsets.leftOrZero).coerceAtLeast( + 0f ), - bottomLeft = CornerRadii( - horizontal = max(0f, borderRadius.bottomLeft.horizontal - borderLeft).dpToPx(), - vertical = max(0f, borderRadius.bottomLeft.vertical - borderBottom).dpToPx() + vertical = (borderRadius.bottomLeft.vertical - borderInsets.bottomOrZero.coerceAtLeast( + 0f + )) + ), + bottomRight = CornerRadii( + horizontal = (borderRadius.bottomRight.horizontal - borderInsets.rightOrZero).coerceAtLeast( + 0f ), - bottomRight = CornerRadii( - horizontal = max(0f, borderRadius.bottomRight.horizontal - borderRight).dpToPx(), - vertical = max(0f, borderRadius.bottomRight.vertical - borderBottom).dpToPx() - ) + vertical = (borderRadius.bottomRight.vertical - borderInsets.bottomOrZero.coerceAtLeast( + 0f + )) + ) ) } GeometryBox.ContentBox -> { - // content-box: reduce border-radius by border width + padding - // padding already includes border width - val paddingLeft = PixelUtil.toDIPFromPixel(view.paddingLeft.toFloat()).roundToInt() - val paddingTop = PixelUtil.toDIPFromPixel(view.paddingTop.toFloat()).roundToInt() - val paddingRight = PixelUtil.toDIPFromPixel(view.paddingRight.toFloat()).roundToInt() - val paddingBottom = PixelUtil.toDIPFromPixel(view.paddingBottom.toFloat()).roundToInt() - + // Content-box: reduce border-radius by border width and padding ComputedBorderRadius( - topLeft = CornerRadii( - horizontal = max(0f, borderRadius.topLeft.horizontal - paddingLeft).dpToPx(), - vertical = max(0f, borderRadius.topLeft.vertical - paddingTop).dpToPx() + topLeft = CornerRadii( + horizontal = (borderRadius.topLeft.horizontal - paddingInsets.leftOrZero - borderInsets.leftOrZero).coerceAtLeast( + 0f + ), + vertical = (borderRadius.topLeft.vertical - paddingInsets.topOrZero - borderInsets.topOrZero.coerceAtLeast( + 0f + )) + ), + topRight = CornerRadii( + horizontal = (borderRadius.topRight.horizontal - paddingInsets.rightOrZero - borderInsets.rightOrZero).coerceAtLeast( + 0f ), - topRight = CornerRadii( - horizontal = max(0f, borderRadius.topRight.horizontal - paddingRight).dpToPx(), - vertical = max(0f, borderRadius.topRight.vertical - paddingTop).dpToPx() + vertical = (borderRadius.topRight.vertical - paddingInsets.topOrZero - borderInsets.topOrZero.coerceAtLeast( + 0f + )) + ), + bottomLeft = CornerRadii( + horizontal = (borderRadius.bottomLeft.horizontal - paddingInsets.leftOrZero - borderInsets.leftOrZero).coerceAtLeast( + 0f ), - bottomLeft = CornerRadii( - horizontal = max(0f, borderRadius.bottomLeft.horizontal - paddingLeft).dpToPx(), - vertical = max(0f, borderRadius.bottomLeft.vertical - paddingBottom).dpToPx() + vertical = (borderRadius.bottomLeft.vertical - paddingInsets.bottomOrZero - borderInsets.bottomOrZero.coerceAtLeast( + 0f + )) + ), + bottomRight = CornerRadii( + horizontal = (borderRadius.bottomRight.horizontal - paddingInsets.rightOrZero - borderInsets.rightOrZero).coerceAtLeast( + 0f ), - bottomRight = CornerRadii( - horizontal = max(0f, borderRadius.bottomRight.horizontal - paddingRight).dpToPx(), - vertical = max(0f, borderRadius.bottomRight.vertical - paddingBottom).dpToPx() - ) + vertical = (borderRadius.bottomRight.vertical - paddingInsets.bottomOrZero - borderInsets.bottomOrZero.coerceAtLeast( + 0f + )) + ) ) } - else -> borderRadius // StrokeBox, ViewBox, FillBox - use border-box as fallback + else -> borderRadius // BorderBox, StrokeBox, ViewBox, FillBox - use border-box as fallback } } @JvmStatic - fun getGeometryBoxBounds(view: View, geometryBox: GeometryBox?, computedBorderInsets: RectF?): RectF { + fun getGeometryBoxBounds( + view: View, + geometryBox: GeometryBox?, + marginInsets: RectF?, + paddingInsets: RectF?, + borderInsets: RectF? + ) + : RectF { val bounds = RectF(0f, 0f, view.width.toFloat(), view.height.toFloat()) - val params = view.layoutParams as? MarginLayoutParams - val box = when (geometryBox) { - GeometryBox.ContentBox -> { - // ContentBox = BorderBox + padding - RectF( - bounds.left + view.paddingLeft, - bounds.top + view.paddingTop, - bounds.right - view.paddingRight, - bounds.bottom - view.paddingBottom + return when (geometryBox) { + GeometryBox.MarginBox -> { + // MarginBox = BorderBox + margin + RectF( + bounds.left - marginInsets.leftOrZero.dpToPx(), + bounds.top - marginInsets.topOrZero.dpToPx(), + bounds.right + marginInsets.rightOrZero.dpToPx(), + bounds.bottom + marginInsets.bottomOrZero.dpToPx() ) - } + } - GeometryBox.PaddingBox -> { - // PaddingBox = BorderBox - border - RectF( - bounds.left + (computedBorderInsets?.left?.dpToPx() ?: 0f), - bounds.top + (computedBorderInsets?.top?.dpToPx() ?: 0f), - bounds.right - (computedBorderInsets?.right?.dpToPx() ?: 0f), - bounds.bottom - (computedBorderInsets?.bottom?.dpToPx() ?: 0f) + GeometryBox.PaddingBox -> { + // PaddingBox = BorderBox - border + RectF( + bounds.left + borderInsets.leftOrZero.dpToPx(), + bounds.top + borderInsets.topOrZero.dpToPx(), + bounds.right - borderInsets.rightOrZero.dpToPx(), + bounds.bottom - borderInsets.bottomOrZero.dpToPx() ) - } + } - GeometryBox.MarginBox -> { - // MarginBox = BorderBox + margin - RectF( - bounds.left - (params?.leftMargin?.dpToPx() ?: 0f), - bounds.top - (params?.topMargin?.dpToPx() ?: 0f), - bounds.right + (params?.rightMargin?.dpToPx() ?: 0f), - bounds.bottom + (params?.bottomMargin?.dpToPx() ?: 0f) + GeometryBox.ContentBox -> { + // ContentBox = BorderBox + padding + RectF( + bounds.left + (borderInsets.leftOrZero + paddingInsets.leftOrZero).dpToPx(), + bounds.top + (borderInsets.topOrZero + paddingInsets.topOrZero).dpToPx(), + bounds.right - (borderInsets.rightOrZero + paddingInsets.rightOrZero).dpToPx(), + bounds.bottom - (borderInsets.bottomOrZero + paddingInsets.bottomOrZero).dpToPx() ) - } - - GeometryBox.BorderBox, null -> { - // BorderBox = view bounds - bounds - } + } - else -> bounds // StrokeBox, ViewBox - use border-box as fallback - } - return box + else -> bounds // BorderBox, StrokeBox, ViewBox - use border-box as fallback + } } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt index 6dbc03edea1e..f42bca8ec52a 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -9,9 +9,6 @@ package com.facebook.react.views.view import android.graphics.Rect import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams -import android.view.ViewGroup.MarginLayoutParams import com.facebook.common.logging.FLog import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.DynamicFromObject @@ -46,22 +43,6 @@ import com.facebook.react.uimanager.style.LogicalEdge @ReactModule(name = ReactViewManager.REACT_CLASS) public open class ReactViewManager : ReactClippingViewManager() { - private enum class MarginIndex { - ALL, - VERTICAL, - HORIZONTAL, - LEFT, - RIGHT, - TOP, - BOTTOM, - START, - END; - - companion object { - fun fromIndex(index: Int): MarginIndex? = values().getOrNull(index) - } - } - public companion object { public const val REACT_CLASS: String = ViewProps.VIEW_CLASS_NAME @@ -389,66 +370,6 @@ public open class ReactViewManager : ReactClippingViewManager() view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing) } -@ReactPropGroup( - names = - [ - ViewProps.MARGIN, - ViewProps.MARGIN_VERTICAL, - ViewProps.MARGIN_HORIZONTAL, - ViewProps.MARGIN_LEFT, - ViewProps.MARGIN_RIGHT, - ViewProps.MARGIN_TOP, - ViewProps.MARGIN_BOTTOM, - ViewProps.MARGIN_START, - ViewProps.MARGIN_END - ], - defaultFloat = Float.NaN, - ) - public open fun setMargin(view: ReactViewGroup, index: Int, margin: Float) { - - val layoutParams = view.layoutParams as? MarginLayoutParams ?: MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) - val leftMargin = layoutParams.leftMargin; - val topMargin = layoutParams.topMargin; - val rightMargin = layoutParams.rightMargin; - val bottomMargin = layoutParams.bottomMargin; - when (MarginIndex.fromIndex(index)) { - MarginIndex.ALL -> { - layoutParams.setMargins(margin.toInt(), margin.toInt(), margin.toInt(), margin.toInt()) - } - MarginIndex.VERTICAL -> { - layoutParams.setMargins(leftMargin, margin.toInt(), rightMargin, margin.toInt()) - } - MarginIndex.HORIZONTAL -> { - layoutParams.setMargins(margin.toInt(), topMargin, margin.toInt(), bottomMargin) - } - MarginIndex.LEFT -> { - layoutParams.setMargins(margin.toInt(), topMargin, rightMargin, bottomMargin) - } - MarginIndex.RIGHT -> { - layoutParams.setMargins(leftMargin, topMargin, margin.toInt(), bottomMargin) - } - MarginIndex.TOP -> { - layoutParams.setMargins(leftMargin, margin.toInt(), rightMargin, bottomMargin) - } - MarginIndex.BOTTOM -> { - layoutParams.setMargins(leftMargin, topMargin, rightMargin, margin.toInt()) - } - MarginIndex.START -> { - layoutParams.setMargins(margin.toInt(), topMargin, rightMargin, bottomMargin) - } - MarginIndex.END -> { - layoutParams.setMargins(leftMargin, topMargin, margin.toInt(), bottomMargin) - } - null -> { - // Unknown index, do nothing - } - } - view.layoutParams = layoutParams - } - - override fun setPadding(view: ReactViewGroup, left: Int, top: Int, right: Int, bottom: Int) { - view.setPadding(left, top, right, bottom) - } @ReactPropGroup( names = diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/ComputedBoxModelRegistry.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/ComputedBoxModelRegistry.cpp new file mode 100644 index 000000000000..a7ec44cf96b7 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/ComputedBoxModelRegistry.cpp @@ -0,0 +1,73 @@ +/* + * 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 "ComputedBoxModelRegistry.h" + +namespace facebook::react { + +void ComputedBoxModelRegistry::store( + SurfaceId surfaceId, + Tag viewTag, + const EdgeInsets& marginInsets, + const EdgeInsets& paddingInsets) { + if (marginInsets == EdgeInsets::ZERO && paddingInsets == EdgeInsets::ZERO) { + remove(surfaceId, viewTag); + return; + } + std::lock_guard lock{mutex_}; + registry_[surfaceId][viewTag] = BoxModelData{marginInsets, paddingInsets}; +} + +std::optional ComputedBoxModelRegistry::getMarginInsets( + SurfaceId surfaceId, + Tag viewTag) const { + auto data = get(surfaceId, viewTag); + return data ? std::optional{data->marginInsets} : std::nullopt; +} + +std::optional ComputedBoxModelRegistry::getPaddingInsets( + SurfaceId surfaceId, + Tag viewTag) const { + auto data = get(surfaceId, viewTag); + return data ? std::optional{data->paddingInsets} : std::nullopt; +} + +void ComputedBoxModelRegistry::remove(SurfaceId surfaceId, Tag viewTag) { + std::lock_guard lock{mutex_}; + auto surfaceIt = registry_.find(surfaceId); + if (surfaceIt != registry_.end()) { + surfaceIt->second.erase(viewTag); + if (surfaceIt->second.empty()) { + registry_.erase(surfaceIt); + } + } +} + +void ComputedBoxModelRegistry::clearSurface(SurfaceId surfaceId) { + std::lock_guard lock{mutex_}; + registry_.erase(surfaceId); +} + +std::optional ComputedBoxModelRegistry::get( + SurfaceId surfaceId, + Tag viewTag) const { + std::lock_guard lock{mutex_}; + auto surfaceIt = registry_.find(surfaceId); + if (surfaceIt == registry_.end()) { + return std::nullopt; + } + + const auto& surfaceData = surfaceIt->second; + auto it = surfaceData.find(viewTag); + if (it == surfaceData.end()) { + return std::nullopt; + } + + return it->second; +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/ComputedBoxModelRegistry.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/ComputedBoxModelRegistry.h new file mode 100644 index 000000000000..9d1935bfaaf6 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/ComputedBoxModelRegistry.h @@ -0,0 +1,84 @@ +/* + * 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 + +#include +#include + +namespace facebook::react { + +/* + * Stores box model data (margins and paddings) for a view. + */ +struct BoxModelData { + EdgeInsets marginInsets{}; + EdgeInsets paddingInsets{}; + + bool operator==(const BoxModelData &other) const { + return marginInsets == other.marginInsets && paddingInsets == other.paddingInsets; + } + + bool operator!=(const BoxModelData &other) const { + return !(*this == other); + } +}; + +/* + * ComputedBoxModelRegistry stores computed margin and padding values for views + * that need them. This is required for clip-path geometry box calculations including + * margin-box and content-box. + */ +class ComputedBoxModelRegistry final { + public: + ComputedBoxModelRegistry() = default; + ComputedBoxModelRegistry(const ComputedBoxModelRegistry&) = delete; + ComputedBoxModelRegistry &operator=(const ComputedBoxModelRegistry&) = delete; + + /* + * Stores both margins and paddings for a view. + */ + void store( + SurfaceId surfaceId, + Tag viewTag, + const EdgeInsets &marginInsets, + const EdgeInsets &paddingInsets); + + /* + * Retrieves computed margins for a view on a given surface. + * Returns std::nullopt if margins are not found. + */ + std::optional getMarginInsets(SurfaceId surfaceId, Tag viewTag) const; + + /* + * Retrieves computed paddings for a view on a given surface. + * Returns std::nullopt if paddings are not found. + */ + std::optional getPaddingInsets(SurfaceId surfaceId, Tag viewTag) const; + + /* + * Removes box model data for a specific view on a given surface. + */ + void remove(SurfaceId surfaceId, Tag viewTag); + + /* + * Clears all box model data for a given surface. + */ + void clearSurface(SurfaceId surfaceId); + + private: + std::optional get(SurfaceId surfaceId, Tag viewTag) const; + + mutable std::mutex mutex_; + std::unordered_map> registry_{}; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index 0ed7581b4c38..28170efbfc9f 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -7,6 +7,7 @@ #include "FabricMountingManager.h" +#include "ComputedBoxModelRegistry.h" #include "EventEmitterWrapper.h" #include "MountItem.h" #include "StateWrapperImpl.h" @@ -16,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -51,6 +54,14 @@ void FabricMountingManager::onSurfaceStart(SurfaceId surfaceId) { void FabricMountingManager::onSurfaceStop(SurfaceId surfaceId) { std::lock_guard lock(allocatedViewsMutex_); allocatedViewRegistry_.erase(surfaceId); + if (computedBoxModelRegistry_) { + computedBoxModelRegistry_->clearSurface(surfaceId); + } +} + +void FabricMountingManager::setComputedBoxModelRegistry( + const std::shared_ptr& registry) { + computedBoxModelRegistry_ = registry; } void FabricMountingManager::onTransactionAvailable(SurfaceId surfaceId) { @@ -71,6 +82,11 @@ bool FabricMountingManager::isViewAllocated(SurfaceId surfaceId, Tag tag) { namespace { +inline bool hasClipPath(const ShadowView& shadowView) { + auto props = std::dynamic_pointer_cast(shadowView.props); + return props && props->clipPath.has_value(); +} + #ifdef REACT_NATIVE_DEBUG // List of layout-only props extracted from ViewProps.kt used to filter out // component props from Props 1.5 to validate the Props 2.0 output @@ -657,6 +673,11 @@ void FabricMountingManager::executeMount( LOG(ERROR) << "Emitting delete for unallocated view " << oldChildShadowView.tag; } + + if (computedBoxModelRegistry_) { + computedBoxModelRegistry_->remove( + surfaceId, oldChildShadowView.tag); + } break; } case ShadowViewMutation::Update: { @@ -713,6 +734,30 @@ void FabricMountingManager::executeMount( CppMountItem::UpdateOverflowInsetMountItem( newChildShadowView)); } + + // Store BoxModel data (margin and padding) only if layout + // information has changed and clipPath prop is present. This + // information is needed for proper calculation of the clipPath + // geometry box. + auto oldHasClipPath = hasClipPath(oldChildShadowView); + auto newHasClipPath = hasClipPath(newChildShadowView); + + if (computedBoxModelRegistry_) { + if (newHasClipPath && + (oldChildShadowView.layoutMetrics.marginInsets != + newChildShadowView.layoutMetrics.marginInsets || + oldChildShadowView.layoutMetrics.paddingInsets != + newChildShadowView.layoutMetrics.paddingInsets)) { + computedBoxModelRegistry_->store( + surfaceId, + newChildShadowView.tag, + newChildShadowView.layoutMetrics.marginInsets, + newChildShadowView.layoutMetrics.paddingInsets); + } else if (oldHasClipPath && !newHasClipPath) { + computedBoxModelRegistry_->remove( + surfaceId, newChildShadowView.tag); + } + } } if (oldChildShadowView.eventEmitter != @@ -796,6 +841,20 @@ void FabricMountingManager::executeMount( CppMountItem::UpdateOverflowInsetMountItem( newChildShadowView)); } + + // Store BoxModel data (margin and padding) only if layout + // information has changed and clipPath prop is present. This + // information is needed for proper calculation of the clipPath + // geometry box. + if (hasClipPath(newChildShadowView)) { + if (computedBoxModelRegistry_) { + computedBoxModelRegistry_->store( + surfaceId, + newChildShadowView.tag, + newChildShadowView.layoutMetrics.marginInsets, + newChildShadowView.layoutMetrics.paddingInsets); + } + } } // EventEmitter diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h index 3d31c58db7eb..d3b53f090cef 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h @@ -17,6 +17,7 @@ namespace facebook::react { +class ComputedBoxModelRegistry; class MountingTransaction; struct ShadowView; @@ -92,6 +93,8 @@ class FabricMountingManager final { void scheduleReactRevisionMerge(SurfaceId surfaceId); + void setComputedBoxModelRegistry(const std::shared_ptr ®istry); + private: bool isOnMainThread(); @@ -111,6 +114,14 @@ class FabricMountingManager final { std::unordered_map> allocatedViewRegistry_{}; std::recursive_mutex allocatedViewsMutex_; + + std::shared_ptr computedBoxModelRegistry_; + + /* + * Calls FabricUIManager.preallocateView() on the Java side if view needs to + * be preallocated. + */ + void preallocateShadowView(const ShadowView &shadowView); }; } // namespace facebook::react diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index 31ccd1e68fd1..c8d17ba1f412 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -10,6 +10,7 @@ #include "AndroidAnimationChoreographer.h" #include "AndroidEventBeat.h" #include "ComponentFactory.h" +#include "ComputedBoxModelRegistry.h" #include "EventBeatManager.h" #include "FabricMountingManager.h" #include "FocusOrderingHelper.h" @@ -35,6 +36,28 @@ namespace facebook::react { +namespace { + +jfloatArray edgeInsetsToFloatArray(const std::optional& insets) { + static constexpr auto SIZE = 4; + if (!insets) { + return nullptr; + } + + auto env = jni::Environment::current(); + auto result = env->NewFloatArray(SIZE); + env->SetFloatArrayRegion( + result, + 0, + SIZE, + std::array{ + insets->left, insets->top, insets->right, insets->bottom} + .data()); + return result; +} + +} // namespace + void FabricUIManagerBinding::initHybrid(jni::alias_ref jobj) { setCxxInstance(jobj); } @@ -596,8 +619,10 @@ void FabricUIManagerBinding::installFabricUIManager( std::unique_lock lock(installMutex_); auto globalJavaUiManager = make_global(javaUIManager); + computedBoxModelRegistry_ = std::make_shared(); mountingManager_ = std::make_shared(globalJavaUiManager); + mountingManager_->setComputedBoxModelRegistry(computedBoxModelRegistry_); std::shared_ptr contextContainer = std::make_shared(); @@ -900,6 +925,24 @@ void FabricUIManagerBinding::onAllAnimationsComplete() { mountingManager->onAllAnimationsComplete(); } +jfloatArray FabricUIManagerBinding::getComputedMarginInsets( + jint surfaceId, + jint viewTag) { + return computedBoxModelRegistry_ + ? edgeInsetsToFloatArray( + computedBoxModelRegistry_->getMarginInsets(surfaceId, viewTag)) + : nullptr; +} + +jfloatArray FabricUIManagerBinding::getComputedPaddingInsets( + jint surfaceId, + jint viewTag) { + return computedBoxModelRegistry_ + ? edgeInsetsToFloatArray( + computedBoxModelRegistry_->getPaddingInsets(surfaceId, viewTag)) + : nullptr; +} + void FabricUIManagerBinding::registerNatives() { registerHybrid({ makeNativeMethod("initHybrid", FabricUIManagerBinding::initHybrid), @@ -944,6 +987,12 @@ void FabricUIManagerBinding::registerNatives() { FabricUIManagerBinding::getRelativeAncestorList), makeNativeMethod( "mergeReactRevision", FabricUIManagerBinding::mergeReactRevision), + makeNativeMethod( + "getComputedMarginInsets", + FabricUIManagerBinding::getComputedMarginInsets), + makeNativeMethod( + "getComputedPaddingInsets", + FabricUIManagerBinding::getComputedPaddingInsets), }); } diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h index ea4b928357ec..212d1f7e43d7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h @@ -28,6 +28,7 @@ namespace facebook::react { class ComponentFactory; +class ComputedBoxModelRegistry; class EventBeatManager; class FabricMountingManager; class Instance; @@ -141,10 +142,15 @@ class FabricUIManagerBinding : public jni::HybridClass, void uninstallFabricUIManager(); + jfloatArray getComputedMarginInsets(jint surfaceId, jint viewTag); + + jfloatArray getComputedPaddingInsets(jint surfaceId, jint viewTag); + // Private member variables std::shared_mutex installMutex_; std::shared_ptr mountingManager_; std::shared_ptr scheduler_; + std::shared_ptr computedBoxModelRegistry_; std::shared_ptr getMountingManager(const char *locationHint); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index 743b0e7a57d3..d91730d7a0e5 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -173,6 +173,19 @@ inline LayoutMetrics layoutMetricsFromYogaNode(yoga::Node &yogaNode) layoutMetrics.layoutDirection = YGNodeLayoutGetDirection(&yogaNode) == YGDirectionRTL ? LayoutDirection::RightToLeft : LayoutDirection::LeftToRight; + layoutMetrics.marginInsets = EdgeInsets{ + floatFromYogaFloat(YGNodeLayoutGetMargin(&yogaNode, YGEdgeLeft)), + floatFromYogaFloat(YGNodeLayoutGetMargin(&yogaNode, YGEdgeTop)), + floatFromYogaFloat(YGNodeLayoutGetMargin(&yogaNode, YGEdgeRight)), + floatFromYogaFloat(YGNodeLayoutGetMargin(&yogaNode, YGEdgeBottom)) + }; + layoutMetrics.paddingInsets = EdgeInsets{ + floatFromYogaFloat(YGNodeLayoutGetPadding(&yogaNode, YGEdgeLeft)), + floatFromYogaFloat(YGNodeLayoutGetPadding(&yogaNode, YGEdgeTop)), + floatFromYogaFloat(YGNodeLayoutGetPadding(&yogaNode, YGEdgeRight)), + floatFromYogaFloat(YGNodeLayoutGetPadding(&yogaNode, YGEdgeBottom)) + }; + return layoutMetrics; } diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.cpp index a0cf58c74ddd..af6bc748e9cb 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.cpp @@ -43,6 +43,16 @@ std::vector getDebugProps( ",right:" + getDebugDescription(object.overflowInset.right, {}) + ",bottom:" + getDebugDescription(object.overflowInset.bottom, {}) + ",left:" + getDebugDescription(object.overflowInset.left, {}) + "}"}, + {.name = "marginInsets", + .value = "{top:" + getDebugDescription(object.marginInsets.top, {}) + + ",right:" + getDebugDescription(object.marginInsets.right, {}) + + ",bottom:" + getDebugDescription(object.marginInsets.bottom, {}) + + ",left:" + getDebugDescription(object.marginInsets.left, {}) + "}"}, + {.name = "paddingInsets", + .value = "{top:" + getDebugDescription(object.paddingInsets.top, {}) + + ",right:" + getDebugDescription(object.paddingInsets.right, {}) + + ",bottom:" + getDebugDescription(object.paddingInsets.bottom, {}) + + ",left:" + getDebugDescription(object.paddingInsets.left, {}) + "}"}, {.name = "displayType", .value = object.displayType == DisplayType::None ? "None" : object.displayType == DisplayType::Flex ? "Flex" diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h index cc20fae6e0f1..8f193b2b8b0c 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h @@ -48,6 +48,11 @@ struct LayoutMetrics { // (like when using `overflow: clip` on Web). EdgeInsets overflowInset{}; + // Width of the margins in each direction. + EdgeInsets marginInsets{}; + // Width of the paddings in each direction. + EdgeInsets paddingInsets{}; + // Origin: the outer border of the node. // Size: includes content only. Rect getContentFrame() const @@ -123,7 +128,9 @@ struct hash { layoutMetrics.layoutDirection, layoutMetrics.pointScaleFactor, layoutMetrics.fontSizeMultiplier, - layoutMetrics.overflowInset); + layoutMetrics.overflowInset, + layoutMetrics.marginInsets, + layoutMetrics.paddingInsets); } }; From edb60f4305a385019c332ee01953e3119e8542db Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Fri, 19 Dec 2025 13:25:41 +0100 Subject: [PATCH 08/18] fix(android): cleanup --- .../main/java/com/facebook/react/views/image/ReactImageView.kt | 2 +- .../com/facebook/react/views/text/PreparedLayoutTextView.kt | 2 ++ .../main/java/com/facebook/react/views/view/ReactViewManager.kt | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 989799c683c4..1f2fa90f2bf5 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -375,7 +375,7 @@ public class ReactImageView( BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) } - super.onDraw(canvas) + super.draw(canvas) if (clipPath != null) { canvas.restore() 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 531ce260ef53..0fdc1109b7fb 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 @@ -135,6 +135,8 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re if (overflow != Overflow.VISIBLE) { BackgroundStyleApplicator.clipToPaddingBox(this, canvas) } + + super.onDraw(canvas) canvas.translate( paddingLeft.toFloat(), paddingTop.toFloat() + (preparedLayout?.verticalOffset ?: 0f), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt index f42bca8ec52a..f3beae6b5859 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -370,7 +370,6 @@ public open class ReactViewManager : ReactClippingViewManager() view.setNeedsOffscreenAlphaCompositing(needsOffscreenAlphaCompositing) } - @ReactPropGroup( names = [ From 94d94f63fb1033577e14d2e3e5cd3fab73fe86a2 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 29 Dec 2025 13:15:23 +0100 Subject: [PATCH 09/18] refactor(android): remove redundant clip path handling in dispatchDraw --- .../com/facebook/react/views/view/ReactViewGroup.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index 26cef7caa3a4..c77d72db29ec 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -919,17 +919,7 @@ public open class ReactViewGroup public constructor(context: Context?) : clipToPaddingBox(this, canvas) } - val clipPath = BackgroundStyleApplicator.getClipPath(this) - if (clipPath != null) { - canvas.save() - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) - } - super.dispatchDraw(canvas) - - if (clipPath != null) { - canvas.restore() - } } override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean { From fbd556f72063560eca27b7840ae08885dbb157e3 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 29 Dec 2025 14:21:20 +0100 Subject: [PATCH 10/18] refactor(android): remove setClipPath method from ReactViewManager --- .../java/com/facebook/react/views/view/ReactViewManager.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt index f3beae6b5859..1f15322897e2 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt @@ -244,12 +244,6 @@ public open class ReactViewManager : ReactClippingViewManager() setBackgroundRepeat(view, backgroundRepeat) } - @ReactProp(name = ViewProps.CLIP_PATH, customType = "ClipPath") - public override fun setClipPath(view: ReactViewGroup, clipPath: ReadableMap?) { - if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) { - BackgroundStyleApplicator.setClipPath(view, clipPath) - } - } @ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID) public open fun nextFocusDown(view: ReactViewGroup, viewId: Int) { From 9c93d888c805180c9690f8f8624b346149d77b5e Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 29 Dec 2025 14:37:54 +0100 Subject: [PATCH 11/18] feat(android): cleaner clip path application in drawing methods --- .../uimanager/BackgroundStyleApplicator.kt | 92 ++++++++++--------- .../react/views/image/ReactImageView.kt | 12 +-- .../views/text/PreparedLayoutTextView.kt | 12 +-- .../react/views/text/ReactTextView.java | 13 +-- .../react/views/textinput/ReactEditText.kt | 12 +-- .../react/views/view/ReactViewGroup.kt | 10 +- 6 files changed, 67 insertions(+), 84 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 6a4769628800..dd52536ce184 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -50,6 +50,7 @@ import com.facebook.react.uimanager.style.GeometryBoxUtil import com.facebook.react.uimanager.style.GeometryBoxUtil.getGeometryBoxBounds import com.facebook.react.uimanager.style.LogicalEdge import com.facebook.react.uimanager.style.OutlineStyle +import androidx.core.graphics.withSave /** * Utility object responsible for applying backgrounds, borders, and related visual effects to @@ -458,56 +459,65 @@ public object BackgroundStyleApplicator { } @JvmStatic - public fun getClipPath(view: View): ClipPath? = view.getTag(R.id.clip_path) as? ClipPath + private fun getClipPath(view: View): ClipPath? = view.getTag(R.id.clip_path) as? ClipPath @JvmStatic - public fun applyClipPathIfPresent(view: View, canvas: Canvas) { - val clipPath = getClipPath(view) ?: return - val composite = getCompositeBackgroundDrawable(view) - val computedMarginInsets = UIManagerHelper.getComputedMarginInsets(view) - val computedPaddingInsets = UIManagerHelper.getComputedPaddingInsets(view) - val computedBorderInsets = - composite?.borderInsets?.resolve(composite.layoutDirection, view.context) - val bounds = getGeometryBoxBounds( - view, - clipPath.geometryBox, - computedMarginInsets, - computedPaddingInsets, - computedBorderInsets - ) - val drawingRect = Rect() - view.getDrawingRect(drawingRect) - bounds.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat()) - - val path: Path? = if (clipPath.shape != null) { - ClipPathUtils.createPathFromBasicShape(clipPath.shape, bounds) - } else if (clipPath.geometryBox != null) { - val borderRadius = composite?.borderRadius - if (borderRadius != null) { - val adjustedBorderRadius = GeometryBoxUtil.adjustBorderRadiusForGeometryBox( - view, clipPath.geometryBox, borderRadius.resolve( - composite.layoutDirection, - view.context, - PixelUtil.toDIPFromPixel(drawingRect.width().toFloat()), - PixelUtil.toDIPFromPixel(drawingRect.height().toFloat()) - ), computedMarginInsets, computedPaddingInsets, computedBorderInsets - ) - if (adjustedBorderRadius != null) { - ClipPathUtils.createRoundedRectPath(bounds, adjustedBorderRadius) + public fun applyClipPathIfPresent(view: View, canvas: Canvas, drawContent: (() -> Unit?)?) { + val clipPath = getClipPath(view) + if (clipPath == null) { + drawContent?.invoke() + return + } + + canvas.withSave { + val composite = getCompositeBackgroundDrawable(view) + val computedMarginInsets = UIManagerHelper.getComputedMarginInsets(view) + val computedPaddingInsets = UIManagerHelper.getComputedPaddingInsets(view) + val computedBorderInsets = + composite?.borderInsets?.resolve(composite.layoutDirection, view.context) + val bounds = getGeometryBoxBounds( + view, + clipPath.geometryBox, + computedMarginInsets, + computedPaddingInsets, + computedBorderInsets + ) + val drawingRect = Rect() + view.getDrawingRect(drawingRect) + bounds.offset(drawingRect.left.toFloat(), drawingRect.top.toFloat()) + + val path: Path? = if (clipPath.shape != null) { + ClipPathUtils.createPathFromBasicShape(clipPath.shape, bounds) + } else if (clipPath.geometryBox != null) { + val borderRadius = composite?.borderRadius + if (borderRadius != null) { + val adjustedBorderRadius = GeometryBoxUtil.adjustBorderRadiusForGeometryBox( + view, clipPath.geometryBox, borderRadius.resolve( + composite.layoutDirection, + view.context, + PixelUtil.toDIPFromPixel(drawingRect.width().toFloat()), + PixelUtil.toDIPFromPixel(drawingRect.height().toFloat()) + ), computedMarginInsets, computedPaddingInsets, computedBorderInsets + ) + if (adjustedBorderRadius != null) { + ClipPathUtils.createRoundedRectPath(bounds, adjustedBorderRadius) + } else { + null + } } else { null } } else { null } - } else { - null - } - if (path != null) { - canvas.clipPath(path) - } else { - canvas.clipRect(bounds) + if (path != null) { + clipPath(path) + } else { + clipRect(bounds) + } + + drawContent?.invoke() } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt index 1f2fa90f2bf5..4bc8216c4209 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageView.kt @@ -369,16 +369,8 @@ public class ReactImageView( public override fun hasOverlappingRendering(): Boolean = false public override fun draw(canvas: Canvas) { - val clipPath = BackgroundStyleApplicator.getClipPath(this) - if (clipPath != null) { - canvas.save() - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) - } - - super.draw(canvas) - - if (clipPath != null) { - canvas.restore() + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) { + super.draw(canvas) } } 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 0fdc1109b7fb..c6fa62797bfe 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 @@ -118,16 +118,8 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re } override fun draw(canvas: Canvas) { - val clipPath = BackgroundStyleApplicator.getClipPath(this) - if (clipPath != null) { - canvas.save() - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) - } - - super.draw(canvas) - - if (clipPath != null) { - canvas.restore() + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) { + super.draw(canvas) } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index 24d7ed274f7e..b7a752d292aa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -230,15 +230,10 @@ protected void onLayout( @Override public void draw(Canvas canvas) { - ClipPath clipPath = (ClipPath) getTag(R.id.clip_path); - if (clipPath != null) { - canvas.save(); - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas); - } - super.draw(canvas); - if (clipPath != null) { - canvas.restore(); - } + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas, () -> { + super.draw(canvas); + return null; + }); } @Override diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index 99aca1ddbf67..75d28d8f1ffe 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -1234,16 +1234,8 @@ public open class ReactEditText public constructor(context: Context) : AppCompat } public override fun draw(canvas: Canvas) { - val clipPath = BackgroundStyleApplicator.getClipPath(this) - if (clipPath != null) { - canvas.save() - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) - } - - super.draw(canvas) - - if (clipPath != null) { - canvas.restore() + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) { + super.draw(canvas) } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index c77d72db29ec..4c64856de981 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -905,12 +905,14 @@ public open class ReactViewGroup public constructor(context: Context?) : (height + -overflowInset.bottom).toFloat(), null, ) - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) - super.draw(canvas) + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) { + super.draw(canvas) + } canvas.restore() } else { - BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) - super.draw(canvas) + BackgroundStyleApplicator.applyClipPathIfPresent(this, canvas) { + super.draw(canvas) + } } } From 5eb8a1846d667a7333bee9b23a1d33f1dd764f24 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 29 Dec 2025 15:04:19 +0100 Subject: [PATCH 12/18] feat(android): move retreiving margin and padding insets to FabricUiManager --- .../react/fabric/FabricUIManager.java | 19 ++++++++--- .../uimanager/BackgroundStyleApplicator.kt | 10 ++++-- .../react/uimanager/UIManagerHelper.kt | 32 ------------------- 3 files changed, 23 insertions(+), 38 deletions(-) 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 dbb0afcbcfd0..44a31803bbd3 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 @@ -21,6 +21,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Point; +import android.graphics.RectF; import android.os.SystemClock; import android.view.View; import android.view.accessibility.AccessibilityEvent; @@ -1485,13 +1486,23 @@ public Map getPerformanceCounters() { } @Nullable - public float[] getComputedMarginInsets(int surfaceId, int viewTag) { - return mBinding != null ? mBinding.getComputedMarginInsets(surfaceId, viewTag) : null; + private RectF computeInsetsToRectF(@Nullable float[] insets, int surfaceId, int viewTag) { + if (insets == null || surfaceId == View.NO_ID || viewTag == View.NO_ID) { + return null; + } + return new RectF(insets[0], insets[1], insets[2], insets[3]); + } + + @Nullable + public RectF getComputedMarginInsets(int surfaceId, int viewTag) { + float[] insets = mBinding != null ? mBinding.getComputedMarginInsets(surfaceId, viewTag) : null; + return computeInsetsToRectF(insets, surfaceId, viewTag); } @Nullable - public float[] getComputedPaddingInsets(int surfaceId, int viewTag) { - return mBinding != null ? mBinding.getComputedPaddingInsets(surfaceId, viewTag) : null; + public RectF getComputedPaddingInsets(int surfaceId, int viewTag) { + float[] insets = mBinding != null ? mBinding.getComputedPaddingInsets(surfaceId, viewTag) : null; + return computeInsetsToRectF(insets, surfaceId, viewTag); } private class MountItemDispatchListener implements MountItemDispatcher.ItemDispatchListener { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index dd52536ce184..69690b06a57f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -51,6 +51,8 @@ import com.facebook.react.uimanager.style.GeometryBoxUtil.getGeometryBoxBounds import com.facebook.react.uimanager.style.LogicalEdge import com.facebook.react.uimanager.style.OutlineStyle import androidx.core.graphics.withSave +import com.facebook.react.bridge.ReactContext +import com.facebook.react.fabric.FabricUIManager /** * Utility object responsible for applying backgrounds, borders, and related visual effects to @@ -471,8 +473,12 @@ public object BackgroundStyleApplicator { canvas.withSave { val composite = getCompositeBackgroundDrawable(view) - val computedMarginInsets = UIManagerHelper.getComputedMarginInsets(view) - val computedPaddingInsets = UIManagerHelper.getComputedPaddingInsets(view) + val uiManager = + UIManagerHelper.getUIManager(view.context as ReactContext, UIManagerType.FABRIC) as? FabricUIManager + val surfaceId = UIManagerHelper.getSurfaceId(view) + val viewId = view.id + val computedMarginInsets = uiManager?.getComputedMarginInsets(surfaceId, viewId) + val computedPaddingInsets = uiManager?.getComputedPaddingInsets(surfaceId, viewId) val computedBorderInsets = composite?.borderInsets?.resolve(composite.layoutDirection, view.context) val bounds = getGeometryBoxBounds( diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.kt index 66445857e273..ae4fdf104e90 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerHelper.kt @@ -11,7 +11,6 @@ package com.facebook.react.uimanager import android.content.Context import android.content.ContextWrapper -import android.graphics.RectF import android.view.View import android.widget.EditText import androidx.core.view.ViewCompat @@ -164,35 +163,4 @@ public object UIManagerHelper { padding[PADDING_BOTTOM_INDEX] = PixelUtil.toDIPFromPixel(editText.paddingBottom.toFloat()) return padding } - - @JvmStatic - public fun getComputedMarginInsets(view: View): RectF? = - getComputedLayoutMetrics(view) { uiManager, surfaceId, viewTag -> - uiManager.getComputedMarginInsets(surfaceId, viewTag) - } - - @JvmStatic - public fun getComputedPaddingInsets(view: View): RectF? = - getComputedLayoutMetrics(view) { uiManager, surfaceId, viewTag -> - uiManager.getComputedPaddingInsets(surfaceId, viewTag) - } - - private inline fun getComputedLayoutMetrics( - view: View, - getter: (com.facebook.react.fabric.FabricUIManager, Int, Int) -> FloatArray? - ): RectF? { - val viewTag = view.id - if (viewTag == View.NO_ID || getUIManagerType(viewTag) != UIManagerType.FABRIC) { - return null - } - - val context = view.context as? ThemedReactContext ?: return null - val surfaceId = getSurfaceId(context) - val uiManager = getUIManager( - context.reactApplicationContext, - UIManagerType.FABRIC - ) as? com.facebook.react.fabric.FabricUIManager ?: return null - val array = getter(uiManager, surfaceId, viewTag) ?: return null - return RectF(array[0], array[1], array[2], array[3]) - } } From b9777980559e5c3cc93cc629f08a2f80de72a4e6 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 29 Dec 2025 15:43:55 +0100 Subject: [PATCH 13/18] feat(android): add NeedsComputedBoxModel trait for clipPath handling --- .../react/fabric/FabricMountingManager.cpp | 20 +++++++++---------- .../components/view/ViewShadowNode.cpp | 6 ++++++ .../react/renderer/core/ShadowNodeTraits.h | 5 +++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index 28170efbfc9f..f3f2cd89ac98 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -82,9 +81,8 @@ bool FabricMountingManager::isViewAllocated(SurfaceId surfaceId, Tag tag) { namespace { -inline bool hasClipPath(const ShadowView& shadowView) { - auto props = std::dynamic_pointer_cast(shadowView.props); - return props && props->clipPath.has_value(); +inline bool needsComputedBoxModel(const ShadowView& shadowView) { + return shadowView.traits.check(ShadowNodeTraits::Trait::NeedsComputedBoxModel); } #ifdef REACT_NATIVE_DEBUG @@ -736,14 +734,14 @@ void FabricMountingManager::executeMount( } // Store BoxModel data (margin and padding) only if layout - // information has changed and clipPath prop is present. This - // information is needed for proper calculation of the clipPath + // information has changed and NeedsComputedBoxModel trait is set. + // This information is needed for proper calculation of the clipPath // geometry box. - auto oldHasClipPath = hasClipPath(oldChildShadowView); - auto newHasClipPath = hasClipPath(newChildShadowView); + auto oldNeedsComputedBoxModel = needsComputedBoxModel(oldChildShadowView); + auto newNeedsComputedBoxModel = needsComputedBoxModel(newChildShadowView); if (computedBoxModelRegistry_) { - if (newHasClipPath && + if (newNeedsComputedBoxModel && (oldChildShadowView.layoutMetrics.marginInsets != newChildShadowView.layoutMetrics.marginInsets || oldChildShadowView.layoutMetrics.paddingInsets != @@ -753,7 +751,7 @@ void FabricMountingManager::executeMount( newChildShadowView.tag, newChildShadowView.layoutMetrics.marginInsets, newChildShadowView.layoutMetrics.paddingInsets); - } else if (oldHasClipPath && !newHasClipPath) { + } else if (oldNeedsComputedBoxModel && !newNeedsComputedBoxModel) { computedBoxModelRegistry_->remove( surfaceId, newChildShadowView.tag); } @@ -846,7 +844,7 @@ void FabricMountingManager::executeMount( // information has changed and clipPath prop is present. This // information is needed for proper calculation of the clipPath // geometry box. - if (hasClipPath(newChildShadowView)) { + if (needsComputedBoxModel(newChildShadowView)) { if (computedBoxModelRegistry_) { computedBoxModelRegistry_->store( surfaceId, diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index a166a90546c6..52ff7d69671c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -85,6 +85,12 @@ void ViewShadowNode::initialize() noexcept { } else { traits_.unset(ShadowNodeTraits::Trait::ChildrenFormStackingContext); } + + if (viewProps.clipPath.has_value()) { + traits_.set(ShadowNodeTraits::Trait::NeedsComputedBoxModel); + } else { + traits_.unset(ShadowNodeTraits::Trait::NeedsComputedBoxModel); + } } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h index cb401a161ced..76ce3c808450 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h +++ b/packages/react-native/ReactCommon/react/renderer/core/ShadowNodeTraits.h @@ -91,6 +91,11 @@ class ShadowNodeTraits { // **Deprecated**: This trait is deprecated and will be removed in a future // version of React Native. DirtyYogaNode = 1 << 14, + + // Indicates that the `ShadowNode` needs to store computed box model + // information (margin and padding) for proper calculation of the clipPath + // geometry box. + NeedsComputedBoxModel = 1 << 15, }; /* From 9c2fc47e39763a85330a5da63fc1d494a9732672 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 29 Dec 2025 15:44:16 +0100 Subject: [PATCH 14/18] refactor(android): remove of getClipPath method --- .../facebook/react/uimanager/BackgroundStyleApplicator.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 69690b06a57f..3bf0a4fd19fb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -460,12 +460,9 @@ public object BackgroundStyleApplicator { view.invalidate() } - @JvmStatic - private fun getClipPath(view: View): ClipPath? = view.getTag(R.id.clip_path) as? ClipPath - @JvmStatic public fun applyClipPathIfPresent(view: View, canvas: Canvas, drawContent: (() -> Unit?)?) { - val clipPath = getClipPath(view) + val clipPath = view.getTag(R.id.clip_path) as? ClipPath if (clipPath == null) { drawContent?.invoke() return From b5db4e7963adcc76a2f06fe7cf16aaa041d05c3f Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Thu, 8 Jan 2026 14:01:51 +0100 Subject: [PATCH 15/18] refactor(android): remove unnecessary blank line --- .../main/java/com/facebook/react/views/view/ReactViewGroup.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt index 4c64856de981..f582a261e315 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt @@ -920,7 +920,6 @@ public open class ReactViewGroup public constructor(context: Context?) : if (_overflow != Overflow.VISIBLE || getTag(R.id.filter) != null) { clipToPaddingBox(this, canvas) } - super.dispatchDraw(canvas) } From f553d7714eced57204ca3a1b33edd22f3f1c1de3 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Mon, 12 Jan 2026 12:47:48 +0100 Subject: [PATCH 16/18] refactor(android): remove unused imports in ReactTextView --- .../main/java/com/facebook/react/views/text/ReactTextView.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java index b7a752d292aa..ab70a23c7704 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java @@ -32,7 +32,6 @@ import androidx.customview.widget.ExploreByTouchHelper; import com.facebook.common.logging.FLog; import com.facebook.infer.annotation.Nullsafe; -import com.facebook.react.R; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.ReactConstants; @@ -46,7 +45,6 @@ import com.facebook.react.uimanager.ViewDefaults; import com.facebook.react.uimanager.style.BorderRadiusProp; import com.facebook.react.uimanager.style.BorderStyle; -import com.facebook.react.uimanager.style.ClipPath; import com.facebook.react.uimanager.style.LogicalEdge; import com.facebook.react.uimanager.style.Overflow; import com.facebook.react.util.AndroidVersion; From 43384ea3610831546af00201eece2007ab23c194 Mon Sep 17 00:00:00 2001 From: Kamil Paradowski Date: Wed, 14 Jan 2026 16:14:39 +0100 Subject: [PATCH 17/18] feat(android): layout insets calculation from YogaStylableProps --- .../react/fabric/FabricMountingManager.cpp | 60 +++++++--- .../renderer/components/view/conversions.h | 111 ++++++++++++++++-- .../react/renderer/core/LayoutMetrics.cpp | 10 -- .../react/renderer/core/LayoutMetrics.h | 9 +- 4 files changed, 140 insertions(+), 50 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp index f3f2cd89ac98..dd569787fe08 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.cpp @@ -82,7 +82,13 @@ bool FabricMountingManager::isViewAllocated(SurfaceId surfaceId, Tag tag) { namespace { inline bool needsComputedBoxModel(const ShadowView& shadowView) { - return shadowView.traits.check(ShadowNodeTraits::Trait::NeedsComputedBoxModel); + return shadowView.traits.check( + ShadowNodeTraits::Trait::NeedsComputedBoxModel); +} + +inline std::shared_ptr getYogaStylableProps( + const ShadowView& shadowView) { + return std::static_pointer_cast(shadowView.props); } #ifdef REACT_NATIVE_DEBUG @@ -737,21 +743,35 @@ void FabricMountingManager::executeMount( // information has changed and NeedsComputedBoxModel trait is set. // This information is needed for proper calculation of the clipPath // geometry box. - auto oldNeedsComputedBoxModel = needsComputedBoxModel(oldChildShadowView); - auto newNeedsComputedBoxModel = needsComputedBoxModel(newChildShadowView); + auto oldNeedsComputedBoxModel = + needsComputedBoxModel(oldChildShadowView); + auto newNeedsComputedBoxModel = + needsComputedBoxModel(newChildShadowView); if (computedBoxModelRegistry_) { - if (newNeedsComputedBoxModel && - (oldChildShadowView.layoutMetrics.marginInsets != - newChildShadowView.layoutMetrics.marginInsets || - oldChildShadowView.layoutMetrics.paddingInsets != - newChildShadowView.layoutMetrics.paddingInsets)) { - computedBoxModelRegistry_->store( - surfaceId, - newChildShadowView.tag, - newChildShadowView.layoutMetrics.marginInsets, - newChildShadowView.layoutMetrics.paddingInsets); - } else if (oldNeedsComputedBoxModel && !newNeedsComputedBoxModel) { + if (newNeedsComputedBoxModel) { + auto oldProps = getYogaStylableProps(oldChildShadowView); + auto newProps = getYogaStylableProps(newChildShadowView); + if (oldProps && newProps) { + auto newMarginInsets = + marginInsetsFromYogaStylableProps(*newProps.get()); + auto oldMarginInsets = + marginInsetsFromYogaStylableProps(*oldProps.get()); + auto newPaddingInsets = + paddingInsetsFromYogaStylableProps(*newProps.get()); + auto oldPaddingInsets = + paddingInsetsFromYogaStylableProps(*oldProps.get()); + if (oldMarginInsets != newMarginInsets || + oldPaddingInsets != newPaddingInsets) { + computedBoxModelRegistry_->store( + surfaceId, + newChildShadowView.tag, + newMarginInsets, + newPaddingInsets); + } + } + } else if ( + oldNeedsComputedBoxModel && !newNeedsComputedBoxModel) { computedBoxModelRegistry_->remove( surfaceId, newChildShadowView.tag); } @@ -846,11 +866,13 @@ void FabricMountingManager::executeMount( // geometry box. if (needsComputedBoxModel(newChildShadowView)) { if (computedBoxModelRegistry_) { - computedBoxModelRegistry_->store( - surfaceId, - newChildShadowView.tag, - newChildShadowView.layoutMetrics.marginInsets, - newChildShadowView.layoutMetrics.paddingInsets); + if (auto newProps = getYogaStylableProps(newChildShadowView)) { + computedBoxModelRegistry_->store( + surfaceId, + newChildShadowView.tag, + marginInsetsFromYogaStylableProps(*newProps.get()), + paddingInsetsFromYogaStylableProps(*newProps.get())); + } } } } diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h index d91730d7a0e5..3d4778371ce7 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/conversions.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -173,22 +174,106 @@ inline LayoutMetrics layoutMetricsFromYogaNode(yoga::Node &yogaNode) layoutMetrics.layoutDirection = YGNodeLayoutGetDirection(&yogaNode) == YGDirectionRTL ? LayoutDirection::RightToLeft : LayoutDirection::LeftToRight; - layoutMetrics.marginInsets = EdgeInsets{ - floatFromYogaFloat(YGNodeLayoutGetMargin(&yogaNode, YGEdgeLeft)), - floatFromYogaFloat(YGNodeLayoutGetMargin(&yogaNode, YGEdgeTop)), - floatFromYogaFloat(YGNodeLayoutGetMargin(&yogaNode, YGEdgeRight)), - floatFromYogaFloat(YGNodeLayoutGetMargin(&yogaNode, YGEdgeBottom)) - }; - layoutMetrics.paddingInsets = EdgeInsets{ - floatFromYogaFloat(YGNodeLayoutGetPadding(&yogaNode, YGEdgeLeft)), - floatFromYogaFloat(YGNodeLayoutGetPadding(&yogaNode, YGEdgeTop)), - floatFromYogaFloat(YGNodeLayoutGetPadding(&yogaNode, YGEdgeRight)), - floatFromYogaFloat(YGNodeLayoutGetPadding(&yogaNode, YGEdgeBottom)) - }; - return layoutMetrics; } +template +float getLayoutEdgeValue(const YogaStylableProps& props, Args&&... args) { + std::optional result; + auto tryResolve = [&](const auto &arg) -> bool { + if (result.has_value()) { + return false; + } + + using T = std::decay_t; + if constexpr (std::is_same_v) { + if (const auto& length = props.*arg; length.isDefined()) { + result = length.value().unwrap(); + } + } else if constexpr (std::is_same_v) { + if (auto edgeValue = (props.yogaStyle.*LayoutMember)(arg); edgeValue.isDefined()) { + result = edgeValue.value().unwrap(); + } + } + return result.has_value(); + }; + + (tryResolve(std::forward(args)) || ...); + return result.value_or(0.0f); +} + +inline EdgeInsets marginInsetsFromYogaStylableProps(const YogaStylableProps &props) +{ + return { + .left = getLayoutEdgeValue<&yoga::Style::margin>( + props, + &YogaStylableProps::marginInlineStart, + facebook::yoga::Edge::Start, + facebook::yoga::Edge::Left, + &YogaStylableProps::marginInline, + facebook::yoga::Edge::Horizontal, + facebook::yoga::Edge::All), + .top = getLayoutEdgeValue<&yoga::Style::margin>( + props, + facebook::yoga::Edge::Top, + &YogaStylableProps::marginBlockStart, + &YogaStylableProps::marginBlock, + facebook::yoga::Edge::Vertical, + facebook::yoga::Edge::All), + .right = getLayoutEdgeValue<&yoga::Style::margin>( + props, + &YogaStylableProps::marginInlineEnd, + facebook::yoga::Edge::End, + facebook::yoga::Edge::Right, + &YogaStylableProps::marginInline, + facebook::yoga::Edge::Horizontal, + facebook::yoga::Edge::All), + .bottom = getLayoutEdgeValue<&yoga::Style::margin>( + props, + facebook::yoga::Edge::Bottom, + &YogaStylableProps::marginBlockEnd, + &YogaStylableProps::marginBlock, + facebook::yoga::Edge::Vertical, + facebook::yoga::Edge::All) + }; +} + +inline EdgeInsets paddingInsetsFromYogaStylableProps(const YogaStylableProps &props) +{ + return { + .left = getLayoutEdgeValue<&yoga::Style::padding>( + props, + &YogaStylableProps::paddingInlineStart, + facebook::yoga::Edge::Start, + facebook::yoga::Edge::Left, + &YogaStylableProps::paddingInline, + facebook::yoga::Edge::Horizontal, + facebook::yoga::Edge::All), + .top = getLayoutEdgeValue<&yoga::Style::padding>( + props, + facebook::yoga::Edge::Top, + &YogaStylableProps::paddingBlockStart, + &YogaStylableProps::paddingBlock, + facebook::yoga::Edge::Vertical, + facebook::yoga::Edge::All), + .right = getLayoutEdgeValue<&yoga::Style::padding>( + props, + &YogaStylableProps::paddingInlineEnd, + facebook::yoga::Edge::End, + facebook::yoga::Edge::Right, + &YogaStylableProps::paddingInline, + facebook::yoga::Edge::Horizontal, + facebook::yoga::Edge::All), + .bottom = getLayoutEdgeValue<&yoga::Style::padding>( + props, + facebook::yoga::Edge::Bottom, + &YogaStylableProps::paddingBlockEnd, + &YogaStylableProps::paddingBlock, + facebook::yoga::Edge::Vertical, + facebook::yoga::Edge::All) + }; +} + inline YGDirection yogaDirectionFromLayoutDirection(LayoutDirection direction) { switch (direction) { diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.cpp index af6bc748e9cb..a0cf58c74ddd 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.cpp @@ -43,16 +43,6 @@ std::vector getDebugProps( ",right:" + getDebugDescription(object.overflowInset.right, {}) + ",bottom:" + getDebugDescription(object.overflowInset.bottom, {}) + ",left:" + getDebugDescription(object.overflowInset.left, {}) + "}"}, - {.name = "marginInsets", - .value = "{top:" + getDebugDescription(object.marginInsets.top, {}) + - ",right:" + getDebugDescription(object.marginInsets.right, {}) + - ",bottom:" + getDebugDescription(object.marginInsets.bottom, {}) + - ",left:" + getDebugDescription(object.marginInsets.left, {}) + "}"}, - {.name = "paddingInsets", - .value = "{top:" + getDebugDescription(object.paddingInsets.top, {}) + - ",right:" + getDebugDescription(object.paddingInsets.right, {}) + - ",bottom:" + getDebugDescription(object.paddingInsets.bottom, {}) + - ",left:" + getDebugDescription(object.paddingInsets.left, {}) + "}"}, {.name = "displayType", .value = object.displayType == DisplayType::None ? "None" : object.displayType == DisplayType::Flex ? "Flex" diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h index 8f193b2b8b0c..cc20fae6e0f1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutMetrics.h @@ -48,11 +48,6 @@ struct LayoutMetrics { // (like when using `overflow: clip` on Web). EdgeInsets overflowInset{}; - // Width of the margins in each direction. - EdgeInsets marginInsets{}; - // Width of the paddings in each direction. - EdgeInsets paddingInsets{}; - // Origin: the outer border of the node. // Size: includes content only. Rect getContentFrame() const @@ -128,9 +123,7 @@ struct hash { layoutMetrics.layoutDirection, layoutMetrics.pointScaleFactor, layoutMetrics.fontSizeMultiplier, - layoutMetrics.overflowInset, - layoutMetrics.marginInsets, - layoutMetrics.paddingInsets); + layoutMetrics.overflowInset); } }; From 1845428038f3b17362fc1e71669d0f9e3474fa18 Mon Sep 17 00:00:00 2001 From: Marcin Szalski Date: Thu, 17 Sep 2026 14:56:58 +0200 Subject: [PATCH 18/18] feat(cpp): add shared C++ clipPath types and prop conversions --- .../uimanager/BackgroundStyleApplicator.kt | 5 +- .../views/text/PreparedLayoutTextView.kt | 1 + .../jni/react/fabric/FabricMountingManager.h | 7 - .../components/view/BaseViewProps.cpp | 24 +- .../renderer/components/view/BaseViewProps.h | 4 + .../view/ClipPathPropsConversions.cpp | 364 ++++++++++++++++++ .../view/ClipPathPropsConversions.h | 32 ++ .../components/view/ViewShadowNode.cpp | 4 +- .../react/renderer/css/CSSCircleShape.h | 73 ++++ .../react/renderer/css/CSSClipPath.h | 162 ++++++++ .../react/renderer/css/CSSEllipseShape.h | 85 ++++ .../react/renderer/css/CSSInsetShape.h | 105 +++++ .../react/renderer/css/CSSPolygonShape.h | 113 ++++++ .../react/renderer/css/CSSRectShape.h | 122 ++++++ .../react/renderer/css/CSSXywhShape.h | 119 ++++++ .../react/renderer/graphics/ClipPath.cpp | 287 ++++++++++++++ .../react/renderer/graphics/ClipPath.h | 156 ++++++++ 17 files changed, 1649 insertions(+), 14 deletions(-) create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.h create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSCircleShape.h create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSClipPath.h create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSEllipseShape.h create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSInsetShape.h create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSPolygonShape.h create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSRectShape.h create mode 100644 packages/react-native/ReactCommon/react/renderer/css/CSSXywhShape.h create mode 100644 packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.h diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt index 3bf0a4fd19fb..26a9cb0c85c0 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt @@ -53,6 +53,7 @@ import com.facebook.react.uimanager.style.OutlineStyle import androidx.core.graphics.withSave import com.facebook.react.bridge.ReactContext import com.facebook.react.fabric.FabricUIManager +import com.facebook.react.uimanager.common.UIManagerType /** * Utility object responsible for applying backgrounds, borders, and related visual effects to @@ -451,10 +452,6 @@ public object BackgroundStyleApplicator { @JvmStatic public fun setClipPath(view: View, clipPathMap: ReadableMap?) { - if (ViewUtil.getUIManagerType(view) != UIManagerType.FABRIC) { - return - } - val clipPath = ClipPath.parse(clipPathMap) view.setTag(R.id.clip_path, clipPath) view.invalidate() 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 c6fa62797bfe..bfd95c995e26 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 @@ -123,6 +123,7 @@ internal class PreparedLayoutTextView(context: Context) : ViewGroup(context), Re } } + @OptIn(UnstableReactNativeAPI::class) override fun onDraw(canvas: Canvas) { if (overflow != Overflow.VISIBLE) { BackgroundStyleApplicator.clipToPaddingBox(this, canvas) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h index d3b53f090cef..9ba5f84d65b7 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricMountingManager.h @@ -41,13 +41,6 @@ class FabricMountingManager final { */ void drainPreallocateViewsQueue(); - /* - * Preallocates a view on the Java side and registers the tag in - * allocatedViewRegistry_ so that executeMount skips the redundant Create - * mount item for this tag. - */ - void preallocateShadowView(const ShadowView &shadowView); - /* * Returns true if the given tag is registered in allocatedViewRegistry_ * for the given surface. A registered tag means executeMount will skip diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 1cb30b0ed6a8..d47a3bef36f0 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -321,7 +322,20 @@ BaseViewProps::BaseViewProps( rawProps, "removeClippedSubviews", sourceProps.removeClippedSubviews, - false)) {} + false)), + clipPath([&]() -> std::unique_ptr { + auto optionalClipPath = convertRawProp( + context, + rawProps, + "clipPath", + sourceProps.clipPath + ? std::make_optional(*sourceProps.clipPath) + : std::nullopt, + std::nullopt); + return optionalClipPath + ? std::make_unique(std::move(*optionalClipPath)) + : nullptr; + }()) {} #define VIEW_EVENT_CASE(eventType) \ case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \ @@ -384,6 +398,14 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(filter); RAW_SET_PROP_SWITCH_CASE_BASIC(boxShadow); RAW_SET_PROP_SWITCH_CASE_BASIC(mixBlendMode); + case CONSTEXPR_RAW_PROPS_KEY_HASH("clipPath"): { + std::optional parsedClipPath; + fromRawValue(context, value, parsedClipPath); + clipPath = parsedClipPath + ? std::make_unique(std::move(*parsedClipPath)) + : nullptr; + return; + } // events field VIEW_EVENT_CASE(PointerEnter); VIEW_EVENT_CASE(PointerEnterCapture); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index c78c4f38729b..5c0789b3d622 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -21,11 +21,13 @@ #include #include #include +#include #include #include #include #include +#include #include namespace facebook::react { @@ -111,6 +113,8 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { bool removeClippedSubviews{false}; + std::unique_ptr clipPath{}; + #pragma mark - Convenience Methods CascadedBorderWidths getBorderWidths() const; diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.cpp new file mode 100644 index 000000000000..d5a4751299f3 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.cpp @@ -0,0 +1,364 @@ +/* + * 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 "ClipPathPropsConversions.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +namespace { +ValueUnit convertLengthPercentageToValueUnit( + const std::variant& value) { + if (std::holds_alternative(value)) { + return {std::get(value).value, UnitType::Point}; + } else { + return {std::get(value).value, UnitType::Percent}; + } +} + +GeometryBox convertCSSGeometryBox(CSSGeometryBox cssBox) { + switch (cssBox) { + case CSSGeometryBox::MarginBox: + return GeometryBox::MarginBox; + case CSSGeometryBox::BorderBox: + return GeometryBox::BorderBox; + case CSSGeometryBox::ContentBox: + return GeometryBox::ContentBox; + case CSSGeometryBox::PaddingBox: + return GeometryBox::PaddingBox; + case CSSGeometryBox::FillBox: + return GeometryBox::FillBox; + case CSSGeometryBox::StrokeBox: + return GeometryBox::StrokeBox; + case CSSGeometryBox::ViewBox: + return GeometryBox::ViewBox; + } +} + +std::optional getOptionalValueUnit( + const std::unordered_map& rawShape, + const std::string& key) { + auto it = rawShape.find(key); + if (it != rawShape.end()) { + return toValueUnit(it->second); + } + return std::nullopt; +} +} // namespace + +std::optional fromCSSClipPath(const CSSClipPath& cssClipPath) { + ClipPath result; + + if (cssClipPath.shape) { + const auto& cssShape = *cssClipPath.shape; + + if (std::holds_alternative(cssShape)) { + auto cssCircle = std::get(cssShape); + CircleShape circle; + if (cssCircle.radius) { + circle.r = convertLengthPercentageToValueUnit(*cssCircle.radius); + } + if (cssCircle.cx) { + circle.cx = convertLengthPercentageToValueUnit(*cssCircle.cx); + } + if (cssCircle.cy) { + circle.cy = convertLengthPercentageToValueUnit(*cssCircle.cy); + } + result.shape = circle; + } else if (std::holds_alternative(cssShape)) { + auto cssEllipse = std::get(cssShape); + EllipseShape ellipse; + if (cssEllipse.rx) { + ellipse.rx = convertLengthPercentageToValueUnit(*cssEllipse.rx); + } + if (cssEllipse.ry) { + ellipse.ry = convertLengthPercentageToValueUnit(*cssEllipse.ry); + } + if (cssEllipse.cx) { + ellipse.cx = convertLengthPercentageToValueUnit(*cssEllipse.cx); + } + if (cssEllipse.cy) { + ellipse.cy = convertLengthPercentageToValueUnit(*cssEllipse.cy); + } + result.shape = ellipse; + } else if (std::holds_alternative(cssShape)) { + auto cssInset = std::get(cssShape); + InsetShape inset; + if (cssInset.top) { + inset.top = convertLengthPercentageToValueUnit(*cssInset.top); + } + if (cssInset.right) { + inset.right = convertLengthPercentageToValueUnit(*cssInset.right); + } + if (cssInset.bottom) { + inset.bottom = convertLengthPercentageToValueUnit(*cssInset.bottom); + } + if (cssInset.left) { + inset.left = convertLengthPercentageToValueUnit(*cssInset.left); + } + if (cssInset.borderRadius) { + inset.borderRadius = + convertLengthPercentageToValueUnit(*cssInset.borderRadius); + } + result.shape = inset; + } else if (std::holds_alternative(cssShape)) { + auto cssPolygon = std::get(cssShape); + PolygonShape polygon; + for (const auto& point : cssPolygon.points) { + polygon.points.push_back( + {convertLengthPercentageToValueUnit(point.first), + convertLengthPercentageToValueUnit(point.second)}); + } + if (cssPolygon.fillRule == CSSFillRule::NonZero) { + polygon.fillRule = FillRule::NonZero; + } else if (cssPolygon.fillRule == CSSFillRule::EvenOdd) { + polygon.fillRule = FillRule::EvenOdd; + } + result.shape = polygon; + } else if (std::holds_alternative(cssShape)) { + auto cssRect = std::get(cssShape); + RectShape rect; + rect.top = convertLengthPercentageToValueUnit(cssRect.top); + rect.right = convertLengthPercentageToValueUnit(cssRect.right); + rect.bottom = convertLengthPercentageToValueUnit(cssRect.bottom); + rect.left = convertLengthPercentageToValueUnit(cssRect.left); + if (cssRect.borderRadius) { + rect.borderRadius = + convertLengthPercentageToValueUnit(*cssRect.borderRadius); + } + result.shape = rect; + } else if (std::holds_alternative(cssShape)) { + auto cssXywh = std::get(cssShape); + XywhShape xywh; + xywh.x = convertLengthPercentageToValueUnit(cssXywh.x); + xywh.y = convertLengthPercentageToValueUnit(cssXywh.y); + xywh.width = convertLengthPercentageToValueUnit(cssXywh.width); + xywh.height = convertLengthPercentageToValueUnit(cssXywh.height); + if (cssXywh.borderRadius) { + xywh.borderRadius = + convertLengthPercentageToValueUnit(*cssXywh.borderRadius); + } + result.shape = xywh; + } + } + + if (cssClipPath.geometryBox) { + result.geometryBox = convertCSSGeometryBox(*cssClipPath.geometryBox); + } + + return result; +} + +void parseProcessedClipPath( + const PropsParserContext& context, + const RawValue& value, + std::optional& result) { + if (!value.hasType>()) { + result = {}; + return; + } + + auto rawClipPath = + static_cast>(value); + ClipPath clipPath; + + auto shapeIt = rawClipPath.find("shape"); + if (shapeIt != rawClipPath.end() && + shapeIt->second.hasType>()) { + auto rawShape = + static_cast>(shapeIt->second); + + auto typeIt = rawShape.find("type"); + if (typeIt == rawShape.end() || !typeIt->second.hasType()) { + result = {}; + return; + } + + auto type = (std::string)(typeIt->second); + + if (type == "inset") { + InsetShape inset; + + if (auto top = getOptionalValueUnit(rawShape, "top")) { + inset.top = *top; + } + if (auto right = getOptionalValueUnit(rawShape, "right")) { + inset.right = *right; + } + if (auto bottom = getOptionalValueUnit(rawShape, "bottom")) { + inset.bottom = *bottom; + } + if (auto left = getOptionalValueUnit(rawShape, "left")) { + inset.left = *left; + } + if (auto borderRadius = getOptionalValueUnit(rawShape, "borderRadius")) { + inset.borderRadius = *borderRadius; + } + + clipPath.shape = inset; + } else if (type == "circle") { + CircleShape circle; + + // r is optional - defaults to 50% handled in rendering + if (auto r = getOptionalValueUnit(rawShape, "r")) { + circle.r = *r; + } + if (auto cx = getOptionalValueUnit(rawShape, "cx")) { + circle.cx = *cx; + } + if (auto cy = getOptionalValueUnit(rawShape, "cy")) { + circle.cy = *cy; + } + + clipPath.shape = circle; + } else if (type == "ellipse") { + EllipseShape ellipse; + + if (auto rx = getOptionalValueUnit(rawShape, "rx")) { + ellipse.rx = *rx; + } + // rx is optional - defaults to 50% handled in rendering + if (auto ry = getOptionalValueUnit(rawShape, "ry")) { + ellipse.ry = *ry; + } + // ry is optional - defaults to 50% handled in rendering + if (auto cx = getOptionalValueUnit(rawShape, "cx")) { + ellipse.cx = *cx; + } + if (auto cy = getOptionalValueUnit(rawShape, "cy")) { + ellipse.cy = *cy; + } + + clipPath.shape = ellipse; + } else if (type == "polygon") { + PolygonShape polygon; + + auto pointsIt = rawShape.find("points"); + if (pointsIt != rawShape.end() && + pointsIt->second.hasType>()) { + auto rawPoints = static_cast>(pointsIt->second); + for (const auto& rawPoint : rawPoints) { + if (rawPoint.hasType>()) { + auto pointMap = + static_cast>( + rawPoint); + auto xIt = pointMap.find("x"); + auto yIt = pointMap.find("y"); + + if (xIt != pointMap.end() && yIt != pointMap.end()) { + polygon.points.push_back( + {toValueUnit(xIt->second), toValueUnit(yIt->second)}); + } + } + } + } + + auto fillRuleIt = rawShape.find("fillRule"); + if (fillRuleIt != rawShape.end() && + fillRuleIt->second.hasType()) { + auto fillRule = (std::string)(fillRuleIt->second); + if (fillRule == "nonzero") { + polygon.fillRule = FillRule::NonZero; + } else if (fillRule == "evenodd") { + polygon.fillRule = FillRule::EvenOdd; + } + } + + clipPath.shape = polygon; + } else if (type == "rect") { + RectShape rect; + + if (auto top = getOptionalValueUnit(rawShape, "top")) { + rect.top = *top; + } + if (auto right = getOptionalValueUnit(rawShape, "right")) { + rect.right = *right; + } + if (auto bottom = getOptionalValueUnit(rawShape, "bottom")) { + rect.bottom = *bottom; + } + if (auto left = getOptionalValueUnit(rawShape, "left")) { + rect.left = *left; + } + if (auto borderRadius = getOptionalValueUnit(rawShape, "borderRadius")) { + rect.borderRadius = *borderRadius; + } + + clipPath.shape = rect; + } else if (type == "xywh") { + XywhShape xywh; + + if (auto x = getOptionalValueUnit(rawShape, "x")) { + xywh.x = *x; + } + if (auto y = getOptionalValueUnit(rawShape, "y")) { + xywh.y = *y; + } + if (auto width = getOptionalValueUnit(rawShape, "width")) { + xywh.width = *width; + } + if (auto height = getOptionalValueUnit(rawShape, "height")) { + xywh.height = *height; + } + if (auto borderRadius = getOptionalValueUnit(rawShape, "borderRadius")) { + xywh.borderRadius = *borderRadius; + } + + clipPath.shape = xywh; + } else { + result = {}; + return; + } + } + + auto geometryBoxIt = rawClipPath.find("geometryBox"); + if (geometryBoxIt != rawClipPath.end() && + geometryBoxIt->second.hasType()) { + auto geometryBox = (std::string)(geometryBoxIt->second); + + if (geometryBox == "border-box") { + clipPath.geometryBox = GeometryBox::BorderBox; + } else if (geometryBox == "padding-box") { + clipPath.geometryBox = GeometryBox::PaddingBox; + } else if (geometryBox == "content-box") { + clipPath.geometryBox = GeometryBox::ContentBox; + } else if (geometryBox == "margin-box") { + clipPath.geometryBox = GeometryBox::MarginBox; + } else if (geometryBox == "fill-box") { + clipPath.geometryBox = GeometryBox::FillBox; + } else if (geometryBox == "stroke-box") { + clipPath.geometryBox = GeometryBox::StrokeBox; + } else if (geometryBox == "view-box") { + clipPath.geometryBox = GeometryBox::ViewBox; + } + } + + result = clipPath; +} + +void parseUnprocessedClipPath( + std::string&& value, + std::optional& result) { + auto clipPath = parseCSSProperty((std::string)value); + if (std::holds_alternative(clipPath)) { + result = {}; + return; + } + + result = fromCSSClipPath(std::get(clipPath)); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.h b/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.h new file mode 100644 index 000000000000..95498227b2cd --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ClipPathPropsConversions.h @@ -0,0 +1,32 @@ +/* + * 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 +#include +#include +#include + +namespace facebook::react { + +void parseProcessedClipPath(const PropsParserContext &context, const RawValue &value, std::optional &result); + +void parseUnprocessedClipPath(std::string &&value, std::optional &result); + +inline void fromRawValue(const PropsParserContext &context, const RawValue &value, std::optional &result) +{ + if (ReactNativeFeatureFlags::enableNativeCSSParsing()) { + parseUnprocessedClipPath((std::string)value, result); + } else { + parseProcessedClipPath(context, value, result); + } +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index 52ff7d69671c..0f89b9d3a84f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -59,7 +59,7 @@ void ViewShadowNode::initialize() noexcept { viewProps.mixBlendMode != BlendMode::Normal || viewProps.isolation == Isolation::Isolate || HostPlatformViewTraitsInitializer::formsStackingContext(viewProps) || - !viewProps.accessibilityOrder.empty(); + !viewProps.accessibilityOrder.empty() || viewProps.clipPath != nullptr; bool formsView = formsStackingContext || isColorMeaningful(viewProps.backgroundColor) || hasBorder() || @@ -86,7 +86,7 @@ void ViewShadowNode::initialize() noexcept { traits_.unset(ShadowNodeTraits::Trait::ChildrenFormStackingContext); } - if (viewProps.clipPath.has_value()) { + if (viewProps.clipPath != nullptr) { traits_.set(ShadowNodeTraits::Trait::NeedsComputedBoxModel); } else { traits_.unset(ShadowNodeTraits::Trait::NeedsComputedBoxModel); diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSCircleShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSCircleShape.h new file mode 100644 index 000000000000..783ccafd85b7 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSCircleShape.h @@ -0,0 +1,73 @@ +/* + * 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 +#include +#include +#include + +namespace facebook::react { + +struct CSSCircleShape { + std::optional> radius; + std::optional> cx; + std::optional> cy; + + bool operator==(const CSSCircleShape &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) + -> std::optional + { + if (!iequals(func.name, "circle")) { + return {}; + } + + CSSCircleShape shape; + + auto radius = parser.parseNextValue(); + if (std::holds_alternative(radius)) { + shape.radius = std::get(radius); + } else if (std::holds_alternative(radius)) { + shape.radius = std::get(radius); + } + parser.syntaxParser().consumeWhitespace(); + auto atResult = parser.syntaxParser().consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("at"); + }); + + if (atResult) { + parser.syntaxParser().consumeWhitespace(); + auto cx = parser.parseNextValue(); + if (std::holds_alternative(cx)) { + shape.cx = std::get(cx); + } else if (std::holds_alternative(cx)) { + shape.cx = std::get(cx); + } + parser.syntaxParser().consumeWhitespace(); + auto cy = parser.parseNextValue(); + if (std::holds_alternative(cy)) { + shape.cy = std::get(cy); + } else if (std::holds_alternative(cy)) { + shape.cy = std::get(cy); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSClipPath.h b/packages/react-native/ReactCommon/react/renderer/css/CSSClipPath.h new file mode 100644 index 000000000000..6958ff29f27a --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSClipPath.h @@ -0,0 +1,162 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +enum class CSSGeometryBox : uint8_t { + BorderBox, + PaddingBox, + ContentBox, + MarginBox, + FillBox, + StrokeBox, + ViewBox, +}; + +template <> +struct CSSDataTypeParser { + static constexpr auto consumePreservedToken(const CSSPreservedToken &token) -> std::optional + { + if (token.type() == CSSTokenType::Ident) { + auto lowercase = fnv1aLowercase(token.stringValue()); + if (lowercase == fnv1a("border-box")) { + return CSSGeometryBox::BorderBox; + } else if (lowercase == fnv1a("padding-box")) { + return CSSGeometryBox::PaddingBox; + } else if (lowercase == fnv1a("content-box")) { + return CSSGeometryBox::ContentBox; + } else if (lowercase == fnv1a("margin-box")) { + return CSSGeometryBox::MarginBox; + } else if (lowercase == fnv1a("fill-box")) { + return CSSGeometryBox::FillBox; + } else if (lowercase == fnv1a("stroke-box")) { + return CSSGeometryBox::StrokeBox; + } else if (lowercase == fnv1a("view-box")) { + return CSSGeometryBox::ViewBox; + } + } + return {}; + } +}; + +static_assert(CSSDataType); + +/** + * Compound type for parsing basic shapes + */ +using CSSBasicShapeTypes = + CSSCompoundDataType; + +/** + * Variant type for basic shapes in clip-path + */ +using CSSBasicShape = CSSVariantWithTypes; + + +/** + * Representation of + * https://www.w3.org/TR/css-masking-1/#the-clip-path + * + * Supports: + * - + * - + * - + * - + */ +struct CSSClipPath { + std::optional shape; + std::optional geometryBox; + + bool operator==(const CSSClipPath &rhs) const + { + return shape == rhs.shape && geometryBox == rhs.geometryBox; + } +}; + +template <> +struct CSSDataTypeParser { + static auto consume(CSSValueParser &parser) -> std::optional + { + auto shape = parser.parseNextValue(); + + if (!std::holds_alternative(shape)) { + auto geometryBox = parser.parseNextValue(CSSDelimiter::Whitespace); + + CSSClipPath result; + if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } else if (std::holds_alternative(shape)) { + result.shape = std::get(shape); + } + + if (std::holds_alternative(geometryBox)) { + result.geometryBox = std::get(geometryBox); + } + + return result; + } + + auto geometryBox = parser.parseNextValue(); + + if (!std::holds_alternative(geometryBox)) { + auto shapeAfter = parser.parseNextValue(CSSDelimiter::Whitespace); + + CSSClipPath result; + result.geometryBox = std::get(geometryBox); + + if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } else if (std::holds_alternative(shapeAfter)) { + result.shape = std::get(shapeAfter); + } + + return result; + } + + return {}; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSEllipseShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSEllipseShape.h new file mode 100644 index 000000000000..3b3d18608f85 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSEllipseShape.h @@ -0,0 +1,85 @@ +/* + * 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 +#include +#include +#include + +namespace facebook::react { + +struct CSSEllipseShape { + std::optional> rx; + std::optional> ry; + std::optional> cx; + std::optional> cy; + + bool operator==(const CSSEllipseShape &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) + -> std::optional + { + if (!iequals(func.name, "ellipse")) { + return {}; + } + + CSSEllipseShape shape; + + auto rx = parser.parseNextValue(); + if (std::holds_alternative(rx)) { + shape.rx = std::get(rx); + } else if (std::holds_alternative(rx)) { + shape.rx = std::get(rx); + } + parser.syntaxParser().consumeWhitespace(); + auto ry = parser.parseNextValue(); + if (std::holds_alternative(ry)) { + shape.ry = std::get(ry); + } else if (std::holds_alternative(ry)) { + shape.ry = std::get(ry); + } else { + shape.ry = shape.rx; + } + + parser.syntaxParser().consumeWhitespace(); + + auto atResult = parser.syntaxParser().consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("at"); + }); + + if (atResult) { + parser.syntaxParser().consumeWhitespace(); + auto cx = parser.parseNextValue(); + if (std::holds_alternative(cx)) { + shape.cx = std::get(cx); + } else if (std::holds_alternative(cx)) { + shape.cx = std::get(cx); + } + parser.syntaxParser().consumeWhitespace(); + auto cy = parser.parseNextValue(); + if (std::holds_alternative(cy)) { + shape.cy = std::get(cy); + } else if (std::holds_alternative(cy)) { + shape.cy = std::get(cy); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSInsetShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSInsetShape.h new file mode 100644 index 000000000000..dae266f05c04 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSInsetShape.h @@ -0,0 +1,105 @@ +/* + * 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 + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct CSSInsetShape { + std::optional> top{}; + std::optional> bottom{}; + std::optional> left{}; + std::optional> right{}; + std::optional> borderRadius{}; + + bool operator==(const CSSInsetShape &rhs) const + { + return top == rhs.top && bottom == rhs.bottom && left == rhs.left && right == rhs.right && + borderRadius == rhs.borderRadius; + } +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) + -> std::optional + { + if (!iequals(func.name, "inset")) { + return {}; + } + + CSSInsetShape shape; + + std::vector> lengths; + for (int i = 0; i < 4; ++i) { + auto length = parser.parseNextValue(); + if (std::holds_alternative(length)) { + lengths.push_back(std::get(length)); + } else if (std::holds_alternative(length)) { + lengths.push_back(std::get(length)); + } else { + break; + } + + parser.syntaxParser().consumeWhitespace(); + } + + if (lengths.empty()) { + return {}; + } + + if (lengths.size() == 1) { + shape.top = shape.right = shape.bottom = shape.left = lengths[0]; + } else if (lengths.size() == 2) { + shape.top = shape.bottom = lengths[0]; + shape.right = shape.left = lengths[1]; + } else if (lengths.size() == 3) { + shape.top = lengths[0]; + shape.right = shape.left = lengths[1]; + shape.bottom = lengths[2]; + } else if (lengths.size() == 4) { + shape.top = lengths[0]; + shape.right = lengths[1]; + shape.bottom = lengths[2]; + shape.left = lengths[3]; + } + + parser.syntaxParser().consumeWhitespace(); + + auto roundResult = parser.syntaxParser().consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("round"); + }); + + if (roundResult) { + parser.syntaxParser().consumeWhitespace(); + auto radius = parser.parseNextValue(); + if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } else if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSPolygonShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSPolygonShape.h new file mode 100644 index 000000000000..0252d0015b9e --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSPolygonShape.h @@ -0,0 +1,113 @@ +/* + * 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 +#include + +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +enum class CSSFillRule : uint8_t { + NonZero, + EvenOdd, +}; + +template <> +struct CSSDataTypeParser { + static auto consumePreservedToken(const CSSPreservedToken &token) -> std::optional + { + if (token.type() == CSSTokenType::Ident) { + auto lowercase = fnv1aLowercase(token.stringValue()); + if (lowercase == fnv1a("nonzero")) { + return CSSFillRule::NonZero; + } else if (lowercase == fnv1a("evenodd")) { + return CSSFillRule::EvenOdd; + } + } + return {}; + } +}; + +static_assert(CSSDataType); + +struct CSSPolygonShape { + std::vector, std::variant>> points; + std::optional fillRule; + + bool operator==(const CSSPolygonShape &rhs) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) + -> std::optional + { + if (!iequals(func.name, "polygon")) { + return {}; + } + + CSSPolygonShape shape; + + auto firstValue = parser.parseNextValue(); + if (std::holds_alternative(firstValue)) { + shape.fillRule = std::get(firstValue); + parser.syntaxParser().consumeDelimiter(CSSDelimiter::Comma); + parser.syntaxParser().consumeWhitespace(); + } + + do { + auto x = parser.parseNextValue(); + if (std::holds_alternative(x)) { + break; + } + + parser.syntaxParser().consumeWhitespace(); + + auto y = parser.parseNextValue(); + if (std::holds_alternative(y)) { + return {}; + } + + std::variant xValue; + std::variant yValue; + + if (std::holds_alternative(x)) { + xValue = std::get(x); + } else if (std::holds_alternative(x)) { + xValue = std::get(x); + } + + if (std::holds_alternative(y)) { + yValue = std::get(y); + } else if (std::holds_alternative(y)) { + yValue = std::get(y); + } + + shape.points.emplace_back(xValue, yValue); + } while (parser.syntaxParser().consumeDelimiter(CSSDelimiter::Comma)); + + if (shape.points.size() < 3) { + return {}; + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSRectShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSRectShape.h new file mode 100644 index 000000000000..a322b5150412 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSRectShape.h @@ -0,0 +1,122 @@ +/* + * 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 +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct CSSRectShape { + std::variant top; + std::variant right; + std::variant bottom; + std::variant left; + std::optional> borderRadius; + + bool operator==(const CSSRectShape &other) const = default; +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) -> std::optional + { + if (!iequals(func.name, "rect")) { + return {}; + } + + auto top = parser.parseNextValue(); + if (std::holds_alternative(top)) { + return std::nullopt; + } + + parser.syntaxParser().consumeWhitespace(); + + auto right = parser.parseNextValue(); + if (std::holds_alternative(right)) { + return std::nullopt; + } + + parser.syntaxParser().consumeWhitespace(); + + auto bottom = parser.parseNextValue(); + if (std::holds_alternative(bottom)) { + return std::nullopt; + } + + parser.syntaxParser().consumeWhitespace(); + + auto left = parser.parseNextValue(); + if (std::holds_alternative(left)) { + return std::nullopt; + } + + CSSRectShape shape; + + if (std::holds_alternative(top)) { + shape.top = std::get(top); + } else if (std::holds_alternative(top)) { + shape.top = std::get(top); + } else if (std::holds_alternative(top)) { + shape.top = CSSPercentage{0.0f}; + } + + if (std::holds_alternative(right)) { + shape.right = std::get(right); + } else if (std::holds_alternative(right)) { + shape.right = std::get(right); + } else if (std::holds_alternative(right)) { + shape.right = CSSPercentage{100.0f}; + } + + if (std::holds_alternative(bottom)) { + shape.bottom = std::get(bottom); + } else if (std::holds_alternative(bottom)) { + shape.bottom = std::get(bottom); + } else if (std::holds_alternative(bottom)) { + shape.bottom = CSSPercentage{100.0f}; + } + + if (std::holds_alternative(left)) { + shape.left = std::get(left); + } else if (std::holds_alternative(left)) { + shape.left = std::get(left); + } else if (std::holds_alternative(left)) { + shape.left = CSSPercentage{0.0f}; + } + + parser.syntaxParser().consumeWhitespace(); + + auto roundResult = parser.syntaxParser().consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("round"); + }); + + if (roundResult) { + parser.syntaxParser().consumeWhitespace(); + auto radius = parser.parseNextValue(); + if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } else if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/css/CSSXywhShape.h b/packages/react-native/ReactCommon/react/renderer/css/CSSXywhShape.h new file mode 100644 index 000000000000..ac952e8aaf67 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/css/CSSXywhShape.h @@ -0,0 +1,119 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +struct CSSXywhShape { + std::variant x; + std::variant y; + std::variant width; + std::variant height; + std::optional> borderRadius; + + bool operator==(const CSSXywhShape &other) const + { + return x == other.x && y == other.y && width == other.width && height == other.height && + borderRadius == other.borderRadius; + } +}; + +template <> +struct CSSDataTypeParser { + static auto consumeFunctionBlock(const CSSFunctionBlock &func, CSSValueParser &parser) -> std::optional + { + if (!iequals(func.name, "xywh")) { + return {}; + } + + auto x = parser.parseNextValue(); + if (std::holds_alternative(x)) { + return std::nullopt; + } + + parser.syntaxParser().consumeWhitespace(); + + auto y = parser.parseNextValue(); + if (std::holds_alternative(y)) { + return std::nullopt; + } + + parser.syntaxParser().consumeWhitespace(); + + auto width = parser.parseNextValue(); + if (std::holds_alternative(width)) { + return std::nullopt; + } + + parser.syntaxParser().consumeWhitespace(); + + auto height = parser.parseNextValue(); + if (std::holds_alternative(height)) { + return std::nullopt; + } + + CSSXywhShape shape; + + if (std::holds_alternative(x)) { + shape.x = std::get(x); + } else if (std::holds_alternative(x)) { + shape.x = std::get(x); + } + + if (std::holds_alternative(y)) { + shape.y = std::get(y); + } else if (std::holds_alternative(y)) { + shape.y = std::get(y); + } + + if (std::holds_alternative(width)) { + shape.width = std::get(width); + } else if (std::holds_alternative(width)) { + shape.width = std::get(width); + } + + if (std::holds_alternative(height)) { + shape.height = std::get(height); + } else if (std::holds_alternative(height)) { + shape.height = std::get(height); + } + + parser.syntaxParser().consumeWhitespace(); + + auto roundResult = parser.syntaxParser().consumeComponentValue([](const CSSPreservedToken &token) -> bool { + return token.type() == CSSTokenType::Ident && fnv1aLowercase(token.stringValue()) == fnv1a("round"); + }); + + if (roundResult) { + parser.syntaxParser().consumeWhitespace(); + auto radius = parser.parseNextValue(); + if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } else if (std::holds_alternative(radius)) { + shape.borderRadius = std::get(radius); + } + } + + return shape; + } +}; + +static_assert(CSSDataType); + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.cpp b/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.cpp new file mode 100644 index 000000000000..ddfa9973b183 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.cpp @@ -0,0 +1,287 @@ +/* + * 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 "ClipPath.h" + +#include +#include +#include +#include + +namespace facebook::react { + +namespace { +std::string geometryBoxToString(facebook::react::GeometryBox box) { + switch (box) { + case facebook::react::GeometryBox::MarginBox: + return "margin-box"; + case facebook::react::GeometryBox::BorderBox: + return "border-box"; + case facebook::react::GeometryBox::ContentBox: + return "content-box"; + case facebook::react::GeometryBox::PaddingBox: + return "padding-box"; + case facebook::react::GeometryBox::FillBox: + return "fill-box"; + case facebook::react::GeometryBox::StrokeBox: + return "stroke-box"; + case facebook::react::GeometryBox::ViewBox: + return "view-box"; + } +} +} // namespace + +bool CircleShape::operator==(const CircleShape& other) const { + return cx == other.cx && cy == other.cy && r == other.r; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void CircleShape::toString(std::stringstream& ss) const { + ss << "circle("; + if (r) { + ss << r->toString(); + } + if (cx || cy) { + ss << " at "; + if (cx) { + ss << cx->toString(); + } + if (cy) { + ss << " " << cy->toString(); + } + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic CircleShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + if (r) { + result["r"] = r->toDynamic(); + } + if (cx) { + result["cx"] = cx->toDynamic(); + } + if (cy) { + result["cy"] = cy->toDynamic(); + } + return result; +} +#endif + +bool EllipseShape::operator==(const EllipseShape& other) const { + return cx == other.cx && cy == other.cy && rx == other.rx && ry == other.ry; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void EllipseShape::toString(std::stringstream& ss) const { + ss << "ellipse("; + if (rx) { + ss << rx->toString(); + } + if (ry) { + ss << " " << ry->toString(); + } + if (cx || cy) { + ss << " at "; + if (cx) { + ss << cx->toString(); + } + if (cy) { + ss << " " << cy->toString(); + } + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic EllipseShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + if (rx) { + result["rx"] = rx->toDynamic(); + } + if (ry) { + result["ry"] = ry->toDynamic(); + } + if (cx) { + result["cx"] = cx->toDynamic(); + } + if (cy) { + result["cy"] = cy->toDynamic(); + } + return result; +} +#endif + +bool InsetShape::operator==(const InsetShape& other) const { + return top == other.top && right == other.right && bottom == other.bottom && + left == other.left && borderRadius == other.borderRadius; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void InsetShape::toString(std::stringstream& ss) const { + ss << "inset(" << top.toString() << " " << right.toString() << " " + << bottom.toString() << " " << left.toString(); + if (borderRadius) { + ss << " round " << borderRadius->toString(); + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic InsetShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + result["top"] = top.toDynamic(); + result["right"] = right.toDynamic(); + result["bottom"] = bottom.toDynamic(); + result["left"] = left.toDynamic(); + if (borderRadius) { + result["borderRadius"] = borderRadius->toDynamic(); + } + return result; +} +#endif + +bool PolygonShape::operator==(const PolygonShape& other) const { + return points == other.points && fillRule == other.fillRule; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void PolygonShape::toString(std::stringstream& ss) const { + ss << "polygon("; + for (size_t i = 0; i < points.size(); i++) { + if (i > 0) { + ss << ", "; + } + ss << points[i].first.toString() << " " << points[i].second.toString(); + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic PolygonShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + folly::dynamic pointsArray = folly::dynamic::array(); + for (const auto& point : points) { + folly::dynamic pointObj = folly::dynamic::object(); + pointObj["x"] = point.first.toDynamic(); + pointObj["y"] = point.second.toDynamic(); + pointsArray.push_back(pointObj); + } + result["points"] = pointsArray; + if (fillRule) { + result["fillRule"] = fillRule == FillRule::EvenOdd ? "evenodd" : "nonzero"; + } + return result; +} +#endif + +bool RectShape::operator==(const RectShape& other) const { + return top == other.top && right == other.right && bottom == other.bottom && + left == other.left && borderRadius == other.borderRadius; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void RectShape::toString(std::stringstream& ss) const { + ss << "rect(" << top.toString() << " " << right.toString() << " " + << bottom.toString() << " " << left.toString() << " "; + if (borderRadius) { + ss << "round " << borderRadius->toString(); + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic RectShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + result["top"] = top.toDynamic(); + result["right"] = right.toDynamic(); + result["bottom"] = bottom.toDynamic(); + result["left"] = left.toDynamic(); + if (borderRadius) { + result["borderRadius"] = borderRadius->toDynamic(); + } + return result; +} +#endif + +bool XywhShape::operator==(const XywhShape& other) const { + return x == other.x && y == other.y && width == other.width && + height == other.height && borderRadius == other.borderRadius; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +void XywhShape::toString(std::stringstream& ss) const { + ss << "xywh(" << x.toString() << " " << y.toString() << " " + << width.toString() << " " << height.toString(); + if (borderRadius) { + ss << " round " << borderRadius->toString(); + } + ss << ")"; +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic XywhShape::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + result["x"] = x.toDynamic(); + result["y"] = y.toDynamic(); + result["width"] = width.toDynamic(); + result["height"] = height.toDynamic(); + if (borderRadius) { + result["borderRadius"] = borderRadius->toDynamic(); + } + return result; +} +#endif + +bool ClipPath::operator==(const ClipPath& other) const { + return shape == other.shape && geometryBox == other.geometryBox; +} + +#if RN_DEBUG_STRING_CONVERTIBLE +std::string ClipPath::toString() const { + std::stringstream ss; + + if (shape) { + std::visit([&](const auto& s) { s.toString(ss); }, *shape); + } + + if (geometryBox) { + if (shape) { + ss << " "; + } + ss << geometryBoxToString(*geometryBox); + } + + return ss.str(); +} +#endif + +#ifdef RN_SERIALIZABLE_STATE +folly::dynamic ClipPath::toDynamic() const { + folly::dynamic result = folly::dynamic::object(); + + if (shape) { + result["shape"] = + std::visit([](const auto& s) { return s.toDynamic(); }, *shape); + } + + if (geometryBox) { + result["geometryBox"] = geometryBoxToString(*geometryBox); + } + + return result; +} +#endif + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.h b/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.h new file mode 100644 index 000000000000..94895b9c96ba --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/graphics/ClipPath.h @@ -0,0 +1,156 @@ +/* + * 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 +#include +#include + +#ifdef RN_SERIALIZABLE_STATE +#include +#endif + +namespace facebook::react { + +struct CircleShape { + std::optional r{}; + std::optional cx{}; + std::optional cy{}; + + bool operator==(const CircleShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +struct EllipseShape { + std::optional rx{}; + std::optional ry{}; + std::optional cx{}; + std::optional cy{}; + + bool operator==(const EllipseShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +struct InsetShape { + ValueUnit top{}; + ValueUnit right{}; + ValueUnit bottom{}; + ValueUnit left{}; + std::optional borderRadius{}; + + bool operator==(const InsetShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +enum class FillRule : uint8_t { + NonZero, + EvenOdd, +}; + +struct PolygonShape { + std::vector> points; + std::optional fillRule; + + bool operator==(const PolygonShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +struct RectShape { + ValueUnit top{}; + ValueUnit right{}; + ValueUnit bottom{}; + ValueUnit left{}; + std::optional borderRadius{}; + + bool operator==(const RectShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +struct XywhShape { + ValueUnit x{}; + ValueUnit y{}; + ValueUnit width{}; + ValueUnit height{}; + std::optional borderRadius{}; + + bool operator==(const XywhShape &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + void toString(std::stringstream &ss) const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +using BasicShape = std::variant; + +enum class GeometryBox : uint8_t { + MarginBox, + BorderBox, + ContentBox, + PaddingBox, + FillBox, + StrokeBox, + ViewBox, +}; + +struct ClipPath { + std::optional shape; + std::optional geometryBox; + + bool operator==(const ClipPath &other) const; + +#if RN_DEBUG_STRING_CONVERTIBLE + std::string toString() const; +#endif + +#ifdef RN_SERIALIZABLE_STATE + folly::dynamic toDynamic() const; +#endif +}; + +} // namespace facebook::react