Skip to content
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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

intergalacticspacehighway
Copy link
Contributor

@intergalacticspacehighway intergalacticspacehighway commented Dec 30, 2024

Summary:

  • Adds support for color transition hint syntax in linear gradients. e.g. linear-gradient(red, 20%, green)
  • Adds px support. Combination of px and % also works.
  • Simplified color stops parsing.
  • The processColorTransitionHint and getFixedColorStops is moved to native code so it can support combination of px and % units as it requires gradient line length, which is derived from view dimensions and gradient line angle.
  • Follows CSS spec (Refer transition hint section) and implementation is referred from blink engine source.

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

Screenshot 2025-01-05 at 11 38 13 PM

Todo

Add testcases for getFixedColorStops and processColorTransitionHint in native code for both platforms. That's the only downside of moving it out of JS 🤦

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Dec 30, 2024
@facebook-github-bot facebook-github-bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Dec 30, 2024
@cipolleschi
Copy link
Contributor

@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.

Comment on lines 59 to 74
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 [];
Copy link
Contributor

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?

Copy link
Contributor Author

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.

#48410 (comment)

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,
Copy link
Contributor

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

Copy link
Contributor Author

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()) {
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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

Copy link
Contributor Author

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},
]);
});
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree, added.

@intergalacticspacehighway intergalacticspacehighway marked this pull request as draft January 5, 2025 07:31
* linear gradient px support

* fix test cases

* handle invalid transition hint

* fix multiple transition hint

* final fixes
@intergalacticspacehighway intergalacticspacehighway marked this pull request as ready for review January 5, 2025 10:58
@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jan 5, 2025

@joevilches i added the px support as well and updated the PR description. sorry it increased the review surface and changes. But it was needed for web parity. Let me know if you want to split it up! thanks 🙏

@intergalacticspacehighway intergalacticspacehighway changed the title feat: linear gradient transition hint syntax feat: linear gradient px and transition hint syntax support Jan 5, 2025
@facebook-github-bot
Copy link
Contributor

@joevilches has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

Comment on lines 26 to 40
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
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, updated. I missed checking that out. Thanks!

i had to add getter for value though, because color stop position need it's own resolver, which relies on gradient line length.

For consistency i also made similar changes to obj c.

Comment on lines 23 to 27
private val _value: Float,
public val type: LengthPercentageType,
) {
public val value: Float
get() = _value
Copy link
Contributor

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

Copy link
Contributor Author

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.

Comment on lines 208 to 215
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
Copy link
Contributor

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
}

@facebook-github-bot
Copy link
Contributor

@joevilches has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

Copy link
Contributor

@joevilches joevilches left a 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
Copy link
Contributor

@joevilches joevilches Jan 13, 2025

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,
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree. Updated.

Comment on lines 389 to 390
LengthPercentageType.POINT -> PixelUtil.toPixelFromDIP(position.value) / gradientLineLength
LengthPercentageType.PERCENT -> position.value / 100
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@facebook-github-bot
Copy link
Contributor

@joevilches has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants