-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: linear gradient px
and transition hint syntax support
#48410
base: main
Are you sure you want to change the base?
feat: linear gradient px
and transition hint syntax support
#48410
Conversation
@intergalacticspacehighway Thanks for the PR! I tagged the people that worked on the feature. Thanks for the patience in this slower time, due to holidays and end-of-the-year celebrations. |
const positions = colorStop.positions; | ||
// Color transition hint syntax (red, 20%, blue) | ||
if ( | ||
colorStop.color == null && | ||
Array.isArray(positions) && | ||
positions.length === 1 | ||
) { | ||
const position = positions[0]; | ||
if (typeof position === 'string' && position.endsWith('%')) { | ||
processedColorStops.push({ | ||
color: null, | ||
position: parseFloat(position) / 100, | ||
}); | ||
} else { | ||
// If a position is invalid, return an empty array and do not apply gradient. Same as web. | ||
return []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can change this to share some code better. Do transition hints have to be percentages? Regardless we are doing a lot of the same things, can you try to get rid of the top level if/else here and share some of this logic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do transition hints have to be percentages?
They are actually length-percentage
in the spec. But i only added %
support for now in color stops. Linear gradient support mixing %
and px
units which require us to know the dimension, for this we'll have to move color stop parsing to native side. I wanted to avoid that change until the new css parser.
can you try to get rid of the top level if/else here and share some of this logic?
i kinda prefer the explicit checks, also each branch has a tiny comment on top, but i will take another look!
type ParsedGradientValue = { | ||
type: 'linearGradient', | ||
direction: LinearGradientDirection, | ||
colorStops: $ReadOnlyArray<{ | ||
color: ProcessedColorValue, | ||
color: ColorStopColor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would comment in what scenarios we can have a null color, or otherwise change the typing to make that super clear like
$ReadOnlyArray<{
colorStop | transitionHint
}>
I think they are similar enough to just roll with the comment but it should be clear that that syntax is supported
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment. Thanks!
@@ -1331,7 +1331,13 @@ inline void fromRawValue( | |||
positionIt->second.hasType<Float>()) { | |||
ColorStop colorStop; | |||
colorStop.position = (Float)(positionIt->second); | |||
fromRawValue(context, colorIt->second, colorStop.color); | |||
if (colorIt->second.hasValue()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the default value of the color in ColorStop? Will we be able to detect that this is a transition hint vs something like the color black?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
here if JS color is null
, that only happens if user passed transition hint. Then the color in ColorStop gets default SharedColor value i.e. HostPlatformColor::UndefinedColor
. So for black color it should be alright!
auto &colorStop = colorStops[i]; | ||
// Skip if not a color hint | ||
if (colorStop.color) { | ||
continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, curious if a color like black breaks this? Regardless I feel like it would be more future proof and more obvious if you just changed the color in ColorStop to be a std::optional or such
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
color in ColorStop is SharedColor instance which defaults to HostPlatformColor::UndefinedColor
and it has bool overload to check for undefined color. So i think black or such colors should be fine?
{color: null, position: 0.4}, | ||
{color: processColor('blue'), position: 1}, | ||
]); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add some more? Hints that are invalid (> 100%, non percents, multiple in a row, etc). Some longer ones with more stops and more hints just to stress test things a bit, etc. The code has a lot of branching logic due to the complexity of the syntax we are allowing, so I think we should err on the side of excessive testing to raise our confidence level here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, added.
* linear gradient px support * fix test cases * handle invalid transition hint * fix multiple transition hint * final fixes
@joevilches i added the |
px
and transition hint syntax support
@joevilches has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
private enum class UnitType { | ||
Point, | ||
Percent, | ||
Undefined | ||
} | ||
|
||
private data class ValueUnit( | ||
val value: Float = 0.0f, | ||
val unit: UnitType = UnitType.Undefined | ||
) | ||
|
||
private data class ColorStop( | ||
var color: Int? = null, | ||
val position: ValueUnit | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@intergalacticspacehighway got some internal comments mentioning we should use LengthPercentage here for position
's type: https://github.com/facebook/react-native/blob/main/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/LengthPercentage.kt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private val _value: Float, | ||
public val type: LengthPercentageType, | ||
) { | ||
public val value: Float | ||
get() = _value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can just change value to be public. We don't have to worry about the setter since its a val
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. Updated. Thanks for reviewing.
if (newPosition == null) { | ||
// Step 1: | ||
// If the first color stop does not have a position, | ||
// set its position to 0%. If the last color stop does not have a position, | ||
// set its position to 100%. | ||
when (i) { | ||
0 -> newPosition = ValueUnit(0f, UnitType.Point) | ||
colorStops.size - 1 -> newPosition = ValueUnit(1f, UnitType.Point) | ||
0 -> newPosition = 0f | ||
colorStops.size - 1 -> newPosition = 1f |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
newPosition = newPosition ?: when (i) {
0 -> 0f
colorStops.size - 1 -> 1f
}
@joevilches has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more things with the new length percentage!
return when (position.type) { | ||
LengthPercentageType.POINT -> PixelUtil.toPixelFromDIP(position.value) / gradientLineLength | ||
LengthPercentageType.PERCENT -> position.value / 100 | ||
else -> null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this switch is exhaustive so this else does nothing, can you remove (this is also an internal linting error)
@@ -20,7 +20,7 @@ public enum class LengthPercentageType { | |||
} | |||
|
|||
public data class LengthPercentage( | |||
private val value: Float, | |||
public val value: Float, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally we do not make this public. Reading the value of a percent has a high chance of being a bug (should resolve it first). I see the current resolve is a bit specific to corner radius. Mind adding a resolve that
- takes a float as the referenceLength
- returns points as is and percents as a percent of said reference length
and use that resolve and make this private again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. Updated.
LengthPercentageType.POINT -> PixelUtil.toPixelFromDIP(position.value) / gradientLineLength | ||
LengthPercentageType.PERCENT -> position.value / 100 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are percents eval'ed relative to the gradientLineLength?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually, px
are eval'ed relative to gradientLineLength 😅. android and iOS linear gradient APIs expect [0-1] value for color stop position. So we need this kind of resolve . e.g. if user passes 50%
we just want to resolve it to 0.5
and If user passes 50px
then we need to resolve it relative to gradientLineLength like 50 / gradientLineLength
.
Lengths are measured along the gradient line from the starting point in the direction of the ending point.
From the color stop syntax spec
tldr; percentage are relative to gradient line length but we do not need to evaluate it as native APIs handle it. For px
we need to eval.
@joevilches has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
Summary:
linear-gradient(red, 20%, green)
px
support. Combination ofpx
and%
also works.processColorTransitionHint
andgetFixedColorStops
is moved to native code so it can support combination ofpx
and%
units as it requires gradient line length, which is derived from view dimensions and gradient line angle.Changelog:
[GENERAL] [ADDED] - Linear gradient color transition hint syntax and
px
unit support.Test Plan:
Added testcase in processBackgroundImage-test.ts and example in LinearGradientExample.js
Todo
Add testcases for
getFixedColorStops
andprocessColorTransitionHint
in native code for both platforms. That's the only downside of moving it out of JS 🤦