From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- layout/painting/nsCSSRenderingGradients.cpp | 1370 +++++++++++++++++++++++++++ 1 file changed, 1370 insertions(+) create mode 100644 layout/painting/nsCSSRenderingGradients.cpp (limited to 'layout/painting/nsCSSRenderingGradients.cpp') diff --git a/layout/painting/nsCSSRenderingGradients.cpp b/layout/painting/nsCSSRenderingGradients.cpp new file mode 100644 index 0000000000..2fb2f0b024 --- /dev/null +++ b/layout/painting/nsCSSRenderingGradients.cpp @@ -0,0 +1,1370 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* utility functions for drawing borders and backgrounds */ + +#include "nsCSSRenderingGradients.h" + +#include + +#include "gfx2DGlue.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Helpers.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/ProfilerLabels.h" + +#include "nsLayoutUtils.h" +#include "nsStyleConsts.h" +#include "nsPresContext.h" +#include "nsPoint.h" +#include "nsRect.h" +#include "nsCSSColorUtils.h" +#include "gfxContext.h" +#include "nsStyleStructInlines.h" +#include "nsCSSProps.h" +#include "gfxUtils.h" +#include "gfxGradientCache.h" + +#include "mozilla/layers/StackingContextHelper.h" +#include "mozilla/layers/WebRenderLayerManager.h" +#include "mozilla/webrender/WebRenderTypes.h" +#include "mozilla/webrender/WebRenderAPI.h" +#include "Units.h" + +#include "mozilla/StaticPrefs_layout.h" + +using namespace mozilla; +using namespace mozilla::gfx; + +static CSSPoint ResolvePosition(const Position& aPos, const CSSSize& aSize) { + CSSCoord h = aPos.horizontal.ResolveToCSSPixels(aSize.width); + CSSCoord v = aPos.vertical.ResolveToCSSPixels(aSize.height); + return CSSPoint(h, v); +} + +// Given a box with size aBoxSize and origin (0,0), and an angle aAngle, +// and a starting point for the gradient line aStart, find the endpoint of +// the gradient line --- the intersection of the gradient line with a line +// perpendicular to aAngle that passes through the farthest corner in the +// direction aAngle. +static CSSPoint ComputeGradientLineEndFromAngle(const CSSPoint& aStart, + double aAngle, + const CSSSize& aBoxSize) { + double dx = cos(-aAngle); + double dy = sin(-aAngle); + CSSPoint farthestCorner(dx > 0 ? aBoxSize.width : 0, + dy > 0 ? aBoxSize.height : 0); + CSSPoint delta = farthestCorner - aStart; + double u = delta.x * dy - delta.y * dx; + return farthestCorner + CSSPoint(-u * dy, u * dx); +} + +// Compute the start and end points of the gradient line for a linear gradient. +static std::tuple ComputeLinearGradientLine( + nsPresContext* aPresContext, const StyleGradient& aGradient, + const CSSSize& aBoxSize) { + using X = StyleHorizontalPositionKeyword; + using Y = StyleVerticalPositionKeyword; + + const StyleLineDirection& direction = aGradient.AsLinear().direction; + const bool isModern = + aGradient.AsLinear().compat_mode == StyleGradientCompatMode::Modern; + + CSSPoint center(aBoxSize.width / 2, aBoxSize.height / 2); + switch (direction.tag) { + case StyleLineDirection::Tag::Angle: { + double angle = direction.AsAngle().ToRadians(); + if (isModern) { + angle = M_PI_2 - angle; + } + CSSPoint end = ComputeGradientLineEndFromAngle(center, angle, aBoxSize); + CSSPoint start = CSSPoint(aBoxSize.width, aBoxSize.height) - end; + return {start, end}; + } + case StyleLineDirection::Tag::Vertical: { + CSSPoint start(center.x, 0); + CSSPoint end(center.x, aBoxSize.height); + if (isModern == (direction.AsVertical() == Y::Top)) { + std::swap(start.y, end.y); + } + return {start, end}; + } + case StyleLineDirection::Tag::Horizontal: { + CSSPoint start(0, center.y); + CSSPoint end(aBoxSize.width, center.y); + if (isModern == (direction.AsHorizontal() == X::Left)) { + std::swap(start.x, end.x); + } + return {start, end}; + } + case StyleLineDirection::Tag::Corner: { + const auto& corner = direction.AsCorner(); + const X& h = corner._0; + const Y& v = corner._1; + + if (isModern) { + float xSign = h == X::Right ? 1.0 : -1.0; + float ySign = v == Y::Top ? 1.0 : -1.0; + double angle = atan2(ySign * aBoxSize.width, xSign * aBoxSize.height); + CSSPoint end = ComputeGradientLineEndFromAngle(center, angle, aBoxSize); + CSSPoint start = CSSPoint(aBoxSize.width, aBoxSize.height) - end; + return {start, end}; + } + + CSSCoord startX = h == X::Left ? 0.0 : aBoxSize.width; + CSSCoord startY = v == Y::Top ? 0.0 : aBoxSize.height; + + CSSPoint start(startX, startY); + CSSPoint end = CSSPoint(aBoxSize.width, aBoxSize.height) - start; + return {start, end}; + } + default: + break; + } + MOZ_ASSERT_UNREACHABLE("Unknown line direction"); + return {CSSPoint(), CSSPoint()}; +} + +using EndingShape = StyleGenericEndingShape; +using RadialGradientRadii = + Variant>; + +static RadialGradientRadii ComputeRadialGradientRadii(const EndingShape& aShape, + const CSSSize& aSize) { + if (aShape.IsCircle()) { + auto& circle = aShape.AsCircle(); + if (circle.IsExtent()) { + return RadialGradientRadii(circle.AsExtent()); + } + CSSCoord radius = circle.AsRadius().ToCSSPixels(); + return RadialGradientRadii(std::make_pair(radius, radius)); + } + auto& ellipse = aShape.AsEllipse(); + if (ellipse.IsExtent()) { + return RadialGradientRadii(ellipse.AsExtent()); + } + + auto& radii = ellipse.AsRadii(); + return RadialGradientRadii( + std::make_pair(radii._0.ResolveToCSSPixels(aSize.width), + radii._1.ResolveToCSSPixels(aSize.height))); +} + +// Compute the start and end points of the gradient line for a radial gradient. +// Also returns the horizontal and vertical radii defining the circle or +// ellipse to use. +static std::tuple +ComputeRadialGradientLine(const StyleGradient& aGradient, + const CSSSize& aBoxSize) { + const auto& radial = aGradient.AsRadial(); + const EndingShape& endingShape = radial.shape; + const Position& position = radial.position; + CSSPoint start = ResolvePosition(position, aBoxSize); + + // Compute gradient shape: the x and y radii of an ellipse. + CSSCoord radiusX, radiusY; + CSSCoord leftDistance = Abs(start.x); + CSSCoord rightDistance = Abs(aBoxSize.width - start.x); + CSSCoord topDistance = Abs(start.y); + CSSCoord bottomDistance = Abs(aBoxSize.height - start.y); + + auto radii = ComputeRadialGradientRadii(endingShape, aBoxSize); + if (radii.is()) { + switch (radii.as()) { + case StyleShapeExtent::ClosestSide: + radiusX = std::min(leftDistance, rightDistance); + radiusY = std::min(topDistance, bottomDistance); + if (endingShape.IsCircle()) { + radiusX = radiusY = std::min(radiusX, radiusY); + } + break; + case StyleShapeExtent::ClosestCorner: { + // Compute x and y distances to nearest corner + CSSCoord offsetX = std::min(leftDistance, rightDistance); + CSSCoord offsetY = std::min(topDistance, bottomDistance); + if (endingShape.IsCircle()) { + radiusX = radiusY = NS_hypot(offsetX, offsetY); + } else { + // maintain aspect ratio + radiusX = offsetX * M_SQRT2; + radiusY = offsetY * M_SQRT2; + } + break; + } + case StyleShapeExtent::FarthestSide: + radiusX = std::max(leftDistance, rightDistance); + radiusY = std::max(topDistance, bottomDistance); + if (endingShape.IsCircle()) { + radiusX = radiusY = std::max(radiusX, radiusY); + } + break; + case StyleShapeExtent::FarthestCorner: { + // Compute x and y distances to nearest corner + CSSCoord offsetX = std::max(leftDistance, rightDistance); + CSSCoord offsetY = std::max(topDistance, bottomDistance); + if (endingShape.IsCircle()) { + radiusX = radiusY = NS_hypot(offsetX, offsetY); + } else { + // maintain aspect ratio + radiusX = offsetX * M_SQRT2; + radiusY = offsetY * M_SQRT2; + } + break; + } + default: + MOZ_ASSERT_UNREACHABLE("Unknown shape extent keyword?"); + radiusX = radiusY = 0; + } + } else { + auto pair = radii.as>(); + radiusX = pair.first; + radiusY = pair.second; + } + + // The gradient line end point is where the gradient line intersects + // the ellipse. + CSSPoint end = start + CSSPoint(radiusX, 0); + return {start, end, radiusX, radiusY}; +} + +// Compute the center and the start angle of the conic gradient. +static std::tuple ComputeConicGradientProperties( + const StyleGradient& aGradient, const CSSSize& aBoxSize) { + const auto& conic = aGradient.AsConic(); + const Position& position = conic.position; + float angle = static_cast(conic.angle.ToRadians()); + CSSPoint center = ResolvePosition(position, aBoxSize); + + return {center, angle}; +} + +static float Interpolate(float aF1, float aF2, float aFrac) { + return aF1 + aFrac * (aF2 - aF1); +} + +static StyleAbsoluteColor Interpolate(const StyleAbsoluteColor& aLeft, + const StyleAbsoluteColor& aRight, + float aFrac) { + // NOTE: This has to match the interpolation method that WebRender uses which + // right now is sRGB. In the future we should implement interpolation in more + // gradient color-spaces. + static constexpr auto kMethod = StyleColorInterpolationMethod{ + StyleColorSpace::Srgb, + StyleHueInterpolationMethod::Shorter, + }; + return Servo_InterpolateColor(kMethod, &aRight, &aLeft, aFrac); +} + +static nscoord FindTileStart(nscoord aDirtyCoord, nscoord aTilePos, + nscoord aTileDim) { + NS_ASSERTION(aTileDim > 0, "Non-positive tile dimension"); + double multiples = floor(double(aDirtyCoord - aTilePos) / aTileDim); + return NSToCoordRound(multiples * aTileDim + aTilePos); +} + +static gfxFloat LinearGradientStopPositionForPoint( + const gfxPoint& aGradientStart, const gfxPoint& aGradientEnd, + const gfxPoint& aPoint) { + gfxPoint d = aGradientEnd - aGradientStart; + gfxPoint p = aPoint - aGradientStart; + /** + * Compute a parameter t such that a line perpendicular to the + * d vector, passing through aGradientStart + d*t, also + * passes through aPoint. + * + * t is given by + * (p.x - d.x*t)*d.x + (p.y - d.y*t)*d.y = 0 + * + * Solving for t we get + * numerator = d.x*p.x + d.y*p.y + * denominator = d.x^2 + d.y^2 + * t = numerator/denominator + * + * In nsCSSRendering::PaintGradient we know the length of d + * is not zero. + */ + double numerator = d.x.value * p.x.value + d.y.value * p.y.value; + double denominator = d.x.value * d.x.value + d.y.value * d.y.value; + return numerator / denominator; +} + +static bool RectIsBeyondLinearGradientEdge(const gfxRect& aRect, + const gfxMatrix& aPatternMatrix, + const nsTArray& aStops, + const gfxPoint& aGradientStart, + const gfxPoint& aGradientEnd, + StyleAbsoluteColor* aOutEdgeColor) { + gfxFloat topLeft = LinearGradientStopPositionForPoint( + aGradientStart, aGradientEnd, + aPatternMatrix.TransformPoint(aRect.TopLeft())); + gfxFloat topRight = LinearGradientStopPositionForPoint( + aGradientStart, aGradientEnd, + aPatternMatrix.TransformPoint(aRect.TopRight())); + gfxFloat bottomLeft = LinearGradientStopPositionForPoint( + aGradientStart, aGradientEnd, + aPatternMatrix.TransformPoint(aRect.BottomLeft())); + gfxFloat bottomRight = LinearGradientStopPositionForPoint( + aGradientStart, aGradientEnd, + aPatternMatrix.TransformPoint(aRect.BottomRight())); + + const ColorStop& firstStop = aStops[0]; + if (topLeft < firstStop.mPosition && topRight < firstStop.mPosition && + bottomLeft < firstStop.mPosition && bottomRight < firstStop.mPosition) { + *aOutEdgeColor = firstStop.mColor; + return true; + } + + const ColorStop& lastStop = aStops.LastElement(); + if (topLeft >= lastStop.mPosition && topRight >= lastStop.mPosition && + bottomLeft >= lastStop.mPosition && bottomRight >= lastStop.mPosition) { + *aOutEdgeColor = lastStop.mColor; + return true; + } + + return false; +} + +static void ResolveMidpoints(nsTArray& stops) { + for (size_t x = 1; x < stops.Length() - 1;) { + if (!stops[x].mIsMidpoint) { + x++; + continue; + } + + const auto& color1 = stops[x - 1].mColor; + const auto& color2 = stops[x + 1].mColor; + float offset1 = stops[x - 1].mPosition; + float offset2 = stops[x + 1].mPosition; + float offset = stops[x].mPosition; + // check if everything coincides. If so, ignore the midpoint. + if (offset - offset1 == offset2 - offset) { + stops.RemoveElementAt(x); + continue; + } + + // Check if we coincide with the left colorstop. + if (offset1 == offset) { + // Morph the midpoint to a regular stop with the color of the next + // color stop. + stops[x].mColor = color2; + stops[x].mIsMidpoint = false; + continue; + } + + // Check if we coincide with the right colorstop. + if (offset2 == offset) { + // Morph the midpoint to a regular stop with the color of the previous + // color stop. + stops[x].mColor = color1; + stops[x].mIsMidpoint = false; + continue; + } + + float midpoint = (offset - offset1) / (offset2 - offset1); + ColorStop newStops[9]; + if (midpoint > .5f) { + for (size_t y = 0; y < 7; y++) { + newStops[y].mPosition = offset1 + (offset - offset1) * (7 + y) / 13; + } + + newStops[7].mPosition = offset + (offset2 - offset) / 3; + newStops[8].mPosition = offset + (offset2 - offset) * 2 / 3; + } else { + newStops[0].mPosition = offset1 + (offset - offset1) / 3; + newStops[1].mPosition = offset1 + (offset - offset1) * 2 / 3; + + for (size_t y = 0; y < 7; y++) { + newStops[y + 2].mPosition = offset + (offset2 - offset) * y / 13; + } + } + // calculate colors + + for (auto& newStop : newStops) { + // Calculate the intermediate color stops per the formula of the CSS + // images spec. http://dev.w3.org/csswg/css-images/#color-stop-syntax 9 + // points were chosen since it is the minimum number of stops that always + // give the smoothest appearace regardless of midpoint position and + // difference in luminance of the end points. + const float relativeOffset = + (newStop.mPosition - offset1) / (offset2 - offset1); + const float multiplier = powf(relativeOffset, logf(.5f) / logf(midpoint)); + + auto srgb1 = color1.ToColorSpace(StyleColorSpace::Srgb); + auto srgb2 = color2.ToColorSpace(StyleColorSpace::Srgb); + + const float red = + srgb1.components._0 + + multiplier * (srgb2.components._0 - srgb1.components._0); + const float green = + srgb1.components._1 + + multiplier * (srgb2.components._1 - srgb1.components._1); + const float blue = + srgb1.components._2 + + multiplier * (srgb2.components._2 - srgb1.components._2); + const float alpha = + srgb1.alpha + multiplier * (srgb2.alpha - srgb1.alpha); + + newStop.mColor = StyleAbsoluteColor::SrgbLegacy(red, green, blue, alpha); + } + + stops.ReplaceElementsAt(x, 1, newStops, 9); + x += 9; + } +} + +static StyleAbsoluteColor TransparentColor(const StyleAbsoluteColor& aColor) { + auto color = aColor; + color.alpha = 0.0f; + return color; +} + +// Adjusts and adds color stops in such a way that drawing the gradient with +// unpremultiplied interpolation looks nearly the same as if it were drawn with +// premultiplied interpolation. +static const float kAlphaIncrementPerGradientStep = 0.1f; +static void ResolvePremultipliedAlpha(nsTArray& aStops) { + for (size_t x = 1; x < aStops.Length(); x++) { + const ColorStop leftStop = aStops[x - 1]; + const ColorStop rightStop = aStops[x]; + + // if the left and right stop have the same alpha value, we don't need + // to do anything. Hardstops should be instant, and also should never + // require dealing with interpolation. + if (leftStop.mColor.alpha == rightStop.mColor.alpha || + leftStop.mPosition == rightStop.mPosition) { + continue; + } + + // Is the stop on the left 100% transparent? If so, have it adopt the color + // of the right stop + if (leftStop.mColor.alpha == 0) { + aStops[x - 1].mColor = TransparentColor(rightStop.mColor); + continue; + } + + // Is the stop on the right completely transparent? + // If so, duplicate it and assign it the color on the left. + if (rightStop.mColor.alpha == 0) { + ColorStop newStop = rightStop; + newStop.mColor = TransparentColor(leftStop.mColor); + aStops.InsertElementAt(x, newStop); + x++; + continue; + } + + // Now handle cases where one or both of the stops are partially + // transparent. + if (leftStop.mColor.alpha != 1.0f || rightStop.mColor.alpha != 1.0f) { + // Calculate how many extra steps. We do a step per 10% transparency. + size_t stepCount = + NSToIntFloor(fabsf(leftStop.mColor.alpha - rightStop.mColor.alpha) / + kAlphaIncrementPerGradientStep); + for (size_t y = 1; y < stepCount; y++) { + float frac = static_cast(y) / stepCount; + ColorStop newStop( + Interpolate(leftStop.mPosition, rightStop.mPosition, frac), false, + Interpolate(leftStop.mColor, rightStop.mColor, frac)); + aStops.InsertElementAt(x, newStop); + x++; + } + } + } +} + +static ColorStop InterpolateColorStop(const ColorStop& aFirst, + const ColorStop& aSecond, + double aPosition, + const StyleAbsoluteColor& aDefault) { + MOZ_ASSERT(aFirst.mPosition <= aPosition); + MOZ_ASSERT(aPosition <= aSecond.mPosition); + + double delta = aSecond.mPosition - aFirst.mPosition; + if (delta < 1e-6) { + return ColorStop(aPosition, false, aDefault); + } + + return ColorStop(aPosition, false, + Interpolate(aFirst.mColor, aSecond.mColor, + (aPosition - aFirst.mPosition) / delta)); +} + +// Clamp and extend the given ColorStop array in-place to fit exactly into the +// range [0, 1]. +static void ClampColorStops(nsTArray& aStops) { + MOZ_ASSERT(aStops.Length() > 0); + + // If all stops are outside the range, then get rid of everything and replace + // with a single colour. + if (aStops.Length() < 2 || aStops[0].mPosition > 1 || + aStops.LastElement().mPosition < 0) { + const auto c = aStops[0].mPosition > 1 ? aStops[0].mColor + : aStops.LastElement().mColor; + aStops.Clear(); + aStops.AppendElement(ColorStop(0, false, c)); + return; + } + + // Create the 0 and 1 points if they fall in the range of |aStops|, and + // discard all stops outside the range [0, 1]. + // XXX: If we have stops positioned at 0 or 1, we only keep the innermost of + // those stops. This should be fine for the current user(s) of this function. + for (size_t i = aStops.Length() - 1; i > 0; i--) { + if (aStops[i - 1].mPosition < 1 && aStops[i].mPosition >= 1) { + // Add a point to position 1. + aStops[i] = + InterpolateColorStop(aStops[i - 1], aStops[i], + /* aPosition = */ 1, aStops[i - 1].mColor); + // Remove all the elements whose position is greater than 1. + aStops.RemoveLastElements(aStops.Length() - (i + 1)); + } + if (aStops[i - 1].mPosition <= 0 && aStops[i].mPosition > 0) { + // Add a point to position 0. + aStops[i - 1] = + InterpolateColorStop(aStops[i - 1], aStops[i], + /* aPosition = */ 0, aStops[i].mColor); + // Remove all of the preceding stops -- they are all negative. + aStops.RemoveElementsAt(0, i - 1); + break; + } + } + + MOZ_ASSERT(aStops[0].mPosition >= -1e6); + MOZ_ASSERT(aStops.LastElement().mPosition - 1 <= 1e6); + + // The end points won't exist yet if they don't fall in the original range of + // |aStops|. Create them if needed. + if (aStops[0].mPosition > 0) { + aStops.InsertElementAt(0, ColorStop(0, false, aStops[0].mColor)); + } + if (aStops.LastElement().mPosition < 1) { + aStops.AppendElement(ColorStop(1, false, aStops.LastElement().mColor)); + } +} + +namespace mozilla { + +template +static StyleAbsoluteColor GetSpecifiedColor( + const StyleGenericGradientItem& aItem, + const ComputedStyle& aStyle) { + if (aItem.IsInterpolationHint()) { + return StyleAbsoluteColor::TRANSPARENT_BLACK; + } + const StyleColor& c = aItem.IsSimpleColorStop() + ? aItem.AsSimpleColorStop() + : aItem.AsComplexColorStop().color; + + return c.ResolveColor(aStyle.StyleText()->mColor); +} + +static Maybe GetSpecifiedGradientPosition( + const StyleGenericGradientItem& aItem, + CSSCoord aLineLength) { + if (aItem.IsSimpleColorStop()) { + return Nothing(); + } + + const LengthPercentage& pos = aItem.IsComplexColorStop() + ? aItem.AsComplexColorStop().position + : aItem.AsInterpolationHint(); + + if (pos.ConvertsToPercentage()) { + return Some(pos.ToPercentage()); + } + + if (aLineLength < 1e-6) { + return Some(0.0); + } + return Some(pos.ResolveToCSSPixels(aLineLength) / aLineLength); +} + +// aLineLength argument is unused for conic-gradients. +static Maybe GetSpecifiedGradientPosition( + const StyleGenericGradientItem& aItem, + CSSCoord aLineLength) { + if (aItem.IsSimpleColorStop()) { + return Nothing(); + } + + const StyleAngleOrPercentage& pos = aItem.IsComplexColorStop() + ? aItem.AsComplexColorStop().position + : aItem.AsInterpolationHint(); + + if (pos.IsPercentage()) { + return Some(pos.AsPercentage()._0); + } + + return Some(pos.AsAngle().ToRadians() / (2 * M_PI)); +} + +template +static nsTArray ComputeColorStopsForItems( + ComputedStyle* aComputedStyle, + Span> aItems, + CSSCoord aLineLength) { + MOZ_ASSERT(aItems.Length() >= 2, + "The parser should reject gradients with less than two stops"); + + nsTArray stops(aItems.Length()); + + // If there is a run of stops before stop i that did not have specified + // positions, then this is the index of the first stop in that run. + Maybe firstUnsetPosition; + for (size_t i = 0; i < aItems.Length(); ++i) { + const auto& stop = aItems[i]; + double position; + + Maybe specifiedPosition = + GetSpecifiedGradientPosition(stop, aLineLength); + + if (specifiedPosition) { + position = *specifiedPosition; + } else if (i == 0) { + // First stop defaults to position 0.0 + position = 0.0; + } else if (i == aItems.Length() - 1) { + // Last stop defaults to position 1.0 + position = 1.0; + } else { + // Other stops with no specified position get their position assigned + // later by interpolation, see below. + // Remember where the run of stops with no specified position starts, + // if it starts here. + if (firstUnsetPosition.isNothing()) { + firstUnsetPosition.emplace(i); + } + MOZ_ASSERT(!stop.IsInterpolationHint(), + "Interpolation hints always specify position"); + auto color = GetSpecifiedColor(stop, *aComputedStyle); + stops.AppendElement(ColorStop(0, false, color)); + continue; + } + + if (i > 0) { + // Prevent decreasing stop positions by advancing this position + // to the previous stop position, if necessary + double previousPosition = firstUnsetPosition + ? stops[*firstUnsetPosition - 1].mPosition + : stops[i - 1].mPosition; + position = std::max(position, previousPosition); + } + auto stopColor = GetSpecifiedColor(stop, *aComputedStyle); + stops.AppendElement( + ColorStop(position, stop.IsInterpolationHint(), stopColor)); + if (firstUnsetPosition) { + // Interpolate positions for all stops that didn't have a specified + // position + double p = stops[*firstUnsetPosition - 1].mPosition; + double d = (stops[i].mPosition - p) / (i - *firstUnsetPosition + 1); + for (size_t j = *firstUnsetPosition; j < i; ++j) { + p += d; + stops[j].mPosition = p; + } + firstUnsetPosition.reset(); + } + } + + return stops; +} + +static nsTArray ComputeColorStops(ComputedStyle* aComputedStyle, + const StyleGradient& aGradient, + CSSCoord aLineLength) { + if (aGradient.IsLinear()) { + return ComputeColorStopsForItems( + aComputedStyle, aGradient.AsLinear().items.AsSpan(), aLineLength); + } + if (aGradient.IsRadial()) { + return ComputeColorStopsForItems( + aComputedStyle, aGradient.AsRadial().items.AsSpan(), aLineLength); + } + return ComputeColorStopsForItems( + aComputedStyle, aGradient.AsConic().items.AsSpan(), aLineLength); +} + +nsCSSGradientRenderer nsCSSGradientRenderer::Create( + nsPresContext* aPresContext, ComputedStyle* aComputedStyle, + const StyleGradient& aGradient, const nsSize& aIntrinsicSize) { + auto srcSize = CSSSize::FromAppUnits(aIntrinsicSize); + + // Compute "gradient line" start and end relative to the intrinsic size of + // the gradient. + CSSPoint lineStart, lineEnd, center; // center is for conic gradients only + CSSCoord radiusX = 0, radiusY = 0; // for radial gradients only + float angle = 0.0; // for conic gradients only + if (aGradient.IsLinear()) { + std::tie(lineStart, lineEnd) = + ComputeLinearGradientLine(aPresContext, aGradient, srcSize); + } else if (aGradient.IsRadial()) { + std::tie(lineStart, lineEnd, radiusX, radiusY) = + ComputeRadialGradientLine(aGradient, srcSize); + } else { + MOZ_ASSERT(aGradient.IsConic()); + std::tie(center, angle) = + ComputeConicGradientProperties(aGradient, srcSize); + } + // Avoid sending Infs or Nans to downwind draw targets. + if (!lineStart.IsFinite() || !lineEnd.IsFinite()) { + lineStart = lineEnd = CSSPoint(0, 0); + } + if (!center.IsFinite()) { + center = CSSPoint(0, 0); + } + CSSCoord lineLength = + NS_hypot(lineEnd.x - lineStart.x, lineEnd.y - lineStart.y); + + // Build color stop array and compute stop positions + nsTArray stops = + ComputeColorStops(aComputedStyle, aGradient, lineLength); + + ResolveMidpoints(stops); + + nsCSSGradientRenderer renderer; + renderer.mPresContext = aPresContext; + renderer.mGradient = &aGradient; + renderer.mStops = std::move(stops); + renderer.mLineStart = { + aPresContext->CSSPixelsToDevPixels(lineStart.x), + aPresContext->CSSPixelsToDevPixels(lineStart.y), + }; + renderer.mLineEnd = { + aPresContext->CSSPixelsToDevPixels(lineEnd.x), + aPresContext->CSSPixelsToDevPixels(lineEnd.y), + }; + renderer.mRadiusX = aPresContext->CSSPixelsToDevPixels(radiusX); + renderer.mRadiusY = aPresContext->CSSPixelsToDevPixels(radiusY); + renderer.mCenter = { + aPresContext->CSSPixelsToDevPixels(center.x), + aPresContext->CSSPixelsToDevPixels(center.y), + }; + renderer.mAngle = angle; + return renderer; +} + +void nsCSSGradientRenderer::Paint(gfxContext& aContext, const nsRect& aDest, + const nsRect& aFillArea, + const nsSize& aRepeatSize, + const CSSIntRect& aSrc, + const nsRect& aDirtyRect, float aOpacity) { + AUTO_PROFILER_LABEL("nsCSSGradientRenderer::Paint", GRAPHICS); + + if (aDest.IsEmpty() || aFillArea.IsEmpty()) { + return; + } + + nscoord appUnitsPerDevPixel = mPresContext->AppUnitsPerDevPixel(); + + gfxFloat lineLength = + NS_hypot(mLineEnd.x - mLineStart.x, mLineEnd.y - mLineStart.y); + bool cellContainsFill = aDest.Contains(aFillArea); + + // If a non-repeating linear gradient is axis-aligned and there are no gaps + // between tiles, we can optimise away most of the work by converting to a + // repeating linear gradient and filling the whole destination rect at once. + bool forceRepeatToCoverTiles = + mGradient->IsLinear() && + (mLineStart.x == mLineEnd.x) != (mLineStart.y == mLineEnd.y) && + aRepeatSize.width == aDest.width && aRepeatSize.height == aDest.height && + !(mGradient->Repeating()) && !aSrc.IsEmpty() && !cellContainsFill; + + gfxMatrix matrix; + if (forceRepeatToCoverTiles) { + // Length of the source rectangle along the gradient axis. + double rectLen; + // The position of the start of the rectangle along the gradient. + double offset; + + // The gradient line is "backwards". Flip the line upside down to make + // things easier, and then rotate the matrix to turn everything back the + // right way up. + if (mLineStart.x > mLineEnd.x || mLineStart.y > mLineEnd.y) { + std::swap(mLineStart, mLineEnd); + matrix.PreScale(-1, -1); + } + + // Fit the gradient line exactly into the source rect. + // aSrc is relative to aIntrinsincSize. + // srcRectDev will be relative to srcSize, so in the same coordinate space + // as lineStart / lineEnd. + gfxRect srcRectDev = nsLayoutUtils::RectToGfxRect( + CSSPixel::ToAppUnits(aSrc), appUnitsPerDevPixel); + if (mLineStart.x != mLineEnd.x) { + rectLen = srcRectDev.width; + offset = (srcRectDev.x - mLineStart.x) / lineLength; + mLineStart.x = srcRectDev.x; + mLineEnd.x = srcRectDev.XMost(); + } else { + rectLen = srcRectDev.height; + offset = (srcRectDev.y - mLineStart.y) / lineLength; + mLineStart.y = srcRectDev.y; + mLineEnd.y = srcRectDev.YMost(); + } + + // Adjust gradient stop positions for the new gradient line. + double scale = lineLength / rectLen; + for (size_t i = 0; i < mStops.Length(); i++) { + mStops[i].mPosition = (mStops[i].mPosition - offset) * fabs(scale); + } + + // Clamp or extrapolate gradient stops to exactly [0, 1]. + ClampColorStops(mStops); + + lineLength = rectLen; + } + + // Eliminate negative-position stops if the gradient is radial. + double firstStop = mStops[0].mPosition; + if (mGradient->IsRadial() && firstStop < 0.0) { + if (mGradient->AsRadial().flags & StyleGradientFlags::REPEATING) { + // Choose an instance of the repeated pattern that gives us all positive + // stop-offsets. + double lastStop = mStops[mStops.Length() - 1].mPosition; + double stopDelta = lastStop - firstStop; + // If all the stops are in approximately the same place then logic below + // will kick in that makes us draw just the last stop color, so don't + // try to do anything in that case. We certainly need to avoid + // dividing by zero. + if (stopDelta >= 1e-6) { + double instanceCount = ceil(-firstStop / stopDelta); + // Advance stops by instanceCount multiples of the period of the + // repeating gradient. + double offset = instanceCount * stopDelta; + for (uint32_t i = 0; i < mStops.Length(); i++) { + mStops[i].mPosition += offset; + } + } + } else { + // Move negative-position stops to position 0.0. We may also need + // to set the color of the stop to the color the gradient should have + // at the center of the ellipse. + for (uint32_t i = 0; i < mStops.Length(); i++) { + double pos = mStops[i].mPosition; + if (pos < 0.0) { + mStops[i].mPosition = 0.0; + // If this is the last stop, we don't need to adjust the color, + // it will fill the entire area. + if (i < mStops.Length() - 1) { + double nextPos = mStops[i + 1].mPosition; + // If nextPos is approximately equal to pos, then we don't + // need to adjust the color of this stop because it's + // not going to be displayed. + // If nextPos is negative, we don't need to adjust the color of + // this stop since it's not going to be displayed because + // nextPos will also be moved to 0.0. + if (nextPos >= 0.0 && nextPos - pos >= 1e-6) { + // Compute how far the new position 0.0 is along the interval + // between pos and nextPos. + // XXX Color interpolation (in cairo, too) should use the + // CSS 'color-interpolation' property! + float frac = float((0.0 - pos) / (nextPos - pos)); + mStops[i].mColor = + Interpolate(mStops[i].mColor, mStops[i + 1].mColor, frac); + } + } + } + } + } + firstStop = mStops[0].mPosition; + MOZ_ASSERT(firstStop >= 0.0, "Failed to fix stop offsets"); + } + + if (mGradient->IsRadial() && + !(mGradient->AsRadial().flags & StyleGradientFlags::REPEATING)) { + // Direct2D can only handle a particular class of radial gradients because + // of the way the it specifies gradients. Setting firstStop to 0, when we + // can, will help us stay on the fast path. Currently we don't do this + // for repeating gradients but we could by adjusting the stop collection + // to start at 0 + firstStop = 0; + } + + double lastStop = mStops[mStops.Length() - 1].mPosition; + // Cairo gradients must have stop positions in the range [0, 1]. So, + // stop positions will be normalized below by subtracting firstStop and then + // multiplying by stopScale. + double stopScale; + double stopOrigin = firstStop; + double stopEnd = lastStop; + double stopDelta = lastStop - firstStop; + bool zeroRadius = + mGradient->IsRadial() && (mRadiusX < 1e-6 || mRadiusY < 1e-6); + if (stopDelta < 1e-6 || (!mGradient->IsConic() && lineLength < 1e-6) || + zeroRadius) { + // Stops are all at the same place. Map all stops to 0.0. + // For repeating radial gradients, or for any radial gradients with + // a zero radius, we need to fill with the last stop color, so just set + // both radii to 0. + if (mGradient->Repeating() || zeroRadius) { + mRadiusX = mRadiusY = 0.0; + } + stopDelta = 0.0; + } + + // Don't normalize non-repeating or degenerate gradients below 0..1 + // This keeps the gradient line as large as the box and doesn't + // lets us avoiding having to get padding correct for stops + // at 0 and 1 + if (!mGradient->Repeating() || stopDelta == 0.0) { + stopOrigin = std::min(stopOrigin, 0.0); + stopEnd = std::max(stopEnd, 1.0); + } + stopScale = 1.0 / (stopEnd - stopOrigin); + + // Create the gradient pattern. + RefPtr gradientPattern; + gfxPoint gradientStart; + gfxPoint gradientEnd; + if (mGradient->IsLinear()) { + // Compute the actual gradient line ends we need to pass to cairo after + // stops have been normalized. + gradientStart = mLineStart + (mLineEnd - mLineStart) * stopOrigin; + gradientEnd = mLineStart + (mLineEnd - mLineStart) * stopEnd; + + if (stopDelta == 0.0) { + // Stops are all at the same place. For repeating gradients, this will + // just paint the last stop color. We don't need to do anything. + // For non-repeating gradients, this should render as two colors, one + // on each "side" of the gradient line segment, which is a point. All + // our stops will be at 0.0; we just need to set the direction vector + // correctly. + gradientEnd = gradientStart + (mLineEnd - mLineStart); + } + + gradientPattern = new gfxPattern(gradientStart.x, gradientStart.y, + gradientEnd.x, gradientEnd.y); + } else if (mGradient->IsRadial()) { + NS_ASSERTION(firstStop >= 0.0, + "Negative stops not allowed for radial gradients"); + + // To form an ellipse, we'll stretch a circle vertically, if necessary. + // So our radii are based on radiusX. + double innerRadius = mRadiusX * stopOrigin; + double outerRadius = mRadiusX * stopEnd; + if (stopDelta == 0.0) { + // Stops are all at the same place. See above (except we now have + // the inside vs. outside of an ellipse). + outerRadius = innerRadius + 1; + } + gradientPattern = new gfxPattern(mLineStart.x, mLineStart.y, innerRadius, + mLineStart.x, mLineStart.y, outerRadius); + if (mRadiusX != mRadiusY) { + // Stretch the circles into ellipses vertically by setting a transform + // in the pattern. + // Recall that this is the transform from user space to pattern space. + // So to stretch the ellipse by factor of P vertically, we scale + // user coordinates by 1/P. + matrix.PreTranslate(mLineStart); + matrix.PreScale(1.0, mRadiusX / mRadiusY); + matrix.PreTranslate(-mLineStart); + } + } else { + gradientPattern = + new gfxPattern(mCenter.x, mCenter.y, mAngle, stopOrigin, stopEnd); + } + // Use a pattern transform to take account of source and dest rects + matrix.PreTranslate(gfxPoint(mPresContext->CSSPixelsToDevPixels(aSrc.x), + mPresContext->CSSPixelsToDevPixels(aSrc.y))); + matrix.PreScale( + gfxFloat(nsPresContext::CSSPixelsToAppUnits(aSrc.width)) / aDest.width, + gfxFloat(nsPresContext::CSSPixelsToAppUnits(aSrc.height)) / aDest.height); + gradientPattern->SetMatrix(matrix); + + if (stopDelta == 0.0) { + // Non-repeating gradient with all stops in same place -> just add + // first stop and last stop, both at position 0. + // Repeating gradient with all stops in the same place, or radial + // gradient with radius of 0 -> just paint the last stop color. + // We use firstStop offset to keep |stops| with same units (will later + // normalize to 0). + auto firstColor(mStops[0].mColor); + auto lastColor(mStops.LastElement().mColor); + mStops.Clear(); + + if (!mGradient->Repeating() && !zeroRadius) { + mStops.AppendElement(ColorStop(firstStop, false, firstColor)); + } + mStops.AppendElement(ColorStop(firstStop, false, lastColor)); + } + + ResolvePremultipliedAlpha(mStops); + + bool isRepeat = mGradient->Repeating() || forceRepeatToCoverTiles; + + // Now set normalized color stops in pattern. + // Offscreen gradient surface cache (not a tile): + // On some backends (e.g. D2D), the GradientStops object holds an offscreen + // surface which is a lookup table used to evaluate the gradient. This surface + // can use much memory (ram and/or GPU ram) and can be expensive to create. So + // we cache it. The cache key correlates 1:1 with the arguments for + // CreateGradientStops (also the implied backend type) Note that GradientStop + // is a simple struct with a stop value (while GradientStops has the surface). + nsTArray rawStops(mStops.Length()); + rawStops.SetLength(mStops.Length()); + for (uint32_t i = 0; i < mStops.Length(); i++) { + rawStops[i].color = ToDeviceColor(mStops[i].mColor); + rawStops[i].color.a *= aOpacity; + rawStops[i].offset = stopScale * (mStops[i].mPosition - stopOrigin); + } + RefPtr gs = + gfxGradientCache::GetOrCreateGradientStops( + aContext.GetDrawTarget(), rawStops, + isRepeat ? gfx::ExtendMode::REPEAT : gfx::ExtendMode::CLAMP); + gradientPattern->SetColorStops(gs); + + // Paint gradient tiles. This isn't terribly efficient, but doing it this + // way is simple and sure to get pixel-snapping right. We could speed things + // up by drawing tiles into temporary surfaces and copying those to the + // destination, but after pixel-snapping tiles may not all be the same size. + nsRect dirty; + if (!dirty.IntersectRect(aDirtyRect, aFillArea)) return; + + gfxRect areaToFill = + nsLayoutUtils::RectToGfxRect(aFillArea, appUnitsPerDevPixel); + gfxRect dirtyAreaToFill = + nsLayoutUtils::RectToGfxRect(dirty, appUnitsPerDevPixel); + dirtyAreaToFill.RoundOut(); + + Matrix ctm = aContext.CurrentMatrix(); + bool isCTMPreservingAxisAlignedRectangles = + ctm.PreservesAxisAlignedRectangles(); + + // xStart/yStart are the top-left corner of the top-left tile. + nscoord xStart = FindTileStart(dirty.x, aDest.x, aRepeatSize.width); + nscoord yStart = FindTileStart(dirty.y, aDest.y, aRepeatSize.height); + nscoord xEnd = forceRepeatToCoverTiles ? xStart + aDest.width : dirty.XMost(); + nscoord yEnd = + forceRepeatToCoverTiles ? yStart + aDest.height : dirty.YMost(); + + if (TryPaintTilesWithExtendMode(aContext, gradientPattern, xStart, yStart, + dirtyAreaToFill, aDest, aRepeatSize, + forceRepeatToCoverTiles)) { + return; + } + + // x and y are the top-left corner of the tile to draw + for (nscoord y = yStart; y < yEnd; y += aRepeatSize.height) { + for (nscoord x = xStart; x < xEnd; x += aRepeatSize.width) { + // The coordinates of the tile + gfxRect tileRect = nsLayoutUtils::RectToGfxRect( + nsRect(x, y, aDest.width, aDest.height), appUnitsPerDevPixel); + // The actual area to fill with this tile is the intersection of this + // tile with the overall area we're supposed to be filling + gfxRect fillRect = + forceRepeatToCoverTiles ? areaToFill : tileRect.Intersect(areaToFill); + // Try snapping the fill rect. Snap its top-left and bottom-right + // independently to preserve the orientation. + gfxPoint snappedFillRectTopLeft = fillRect.TopLeft(); + gfxPoint snappedFillRectTopRight = fillRect.TopRight(); + gfxPoint snappedFillRectBottomRight = fillRect.BottomRight(); + // Snap three points instead of just two to ensure we choose the + // correct orientation if there's a reflection. + if (isCTMPreservingAxisAlignedRectangles && + aContext.UserToDevicePixelSnapped(snappedFillRectTopLeft, true) && + aContext.UserToDevicePixelSnapped(snappedFillRectBottomRight, true) && + aContext.UserToDevicePixelSnapped(snappedFillRectTopRight, true)) { + if (snappedFillRectTopLeft.x == snappedFillRectBottomRight.x || + snappedFillRectTopLeft.y == snappedFillRectBottomRight.y) { + // Nothing to draw; avoid scaling by zero and other weirdness that + // could put the context in an error state. + continue; + } + // Set the context's transform to the transform that maps fillRect to + // snappedFillRect. The part of the gradient that was going to + // exactly fill fillRect will fill snappedFillRect instead. + gfxMatrix transform = gfxUtils::TransformRectToRect( + fillRect, snappedFillRectTopLeft, snappedFillRectTopRight, + snappedFillRectBottomRight); + aContext.SetMatrixDouble(transform); + } + aContext.NewPath(); + aContext.Rectangle(fillRect); + + gfxRect dirtyFillRect = fillRect.Intersect(dirtyAreaToFill); + gfxRect fillRectRelativeToTile = dirtyFillRect - tileRect.TopLeft(); + auto edgeColor = StyleAbsoluteColor::TRANSPARENT_BLACK; + if (mGradient->IsLinear() && !isRepeat && + RectIsBeyondLinearGradientEdge(fillRectRelativeToTile, matrix, mStops, + gradientStart, gradientEnd, + &edgeColor)) { + edgeColor.alpha *= aOpacity; + aContext.SetColor(ToSRGBColor(edgeColor)); + } else { + aContext.SetMatrixDouble( + aContext.CurrentMatrixDouble().Copy().PreTranslate( + tileRect.TopLeft())); + aContext.SetPattern(gradientPattern); + } + aContext.Fill(); + aContext.SetMatrix(ctm); + } + } +} + +bool nsCSSGradientRenderer::TryPaintTilesWithExtendMode( + gfxContext& aContext, gfxPattern* aGradientPattern, nscoord aXStart, + nscoord aYStart, const gfxRect& aDirtyAreaToFill, const nsRect& aDest, + const nsSize& aRepeatSize, bool aForceRepeatToCoverTiles) { + // If we have forced a non-repeating gradient to repeat to cover tiles, + // then it will be faster to just paint it once using that optimization + if (aForceRepeatToCoverTiles) { + return false; + } + + nscoord appUnitsPerDevPixel = mPresContext->AppUnitsPerDevPixel(); + + // We can only use this fast path if we don't have to worry about pixel + // snapping, and there is no spacing between tiles. We could handle spacing + // by increasing the size of tileSurface and leaving it transparent, but I'm + // not sure it's worth it + bool canUseExtendModeForTiling = (aXStart % appUnitsPerDevPixel == 0) && + (aYStart % appUnitsPerDevPixel == 0) && + (aDest.width % appUnitsPerDevPixel == 0) && + (aDest.height % appUnitsPerDevPixel == 0) && + (aRepeatSize.width == aDest.width) && + (aRepeatSize.height == aDest.height); + + if (!canUseExtendModeForTiling) { + return false; + } + + IntSize tileSize{ + NSAppUnitsToIntPixels(aDest.width, appUnitsPerDevPixel), + NSAppUnitsToIntPixels(aDest.height, appUnitsPerDevPixel), + }; + + // Check whether this is a reasonable surface size and doesn't overflow + // before doing calculations with the tile size + if (!Factory::ReasonableSurfaceSize(tileSize)) { + return false; + } + + // We only want to do this when there are enough tiles to justify the + // overhead of painting to an offscreen surface. The heuristic here + // is when we will be painting at least 16 tiles or more, this is kind + // of arbitrary + bool shouldUseExtendModeForTiling = + aDirtyAreaToFill.Area() > (tileSize.width * tileSize.height) * 16.0; + + if (!shouldUseExtendModeForTiling) { + return false; + } + + // Draw the gradient pattern into a surface for our single tile + RefPtr tileSurface; + { + RefPtr tileTarget = + aContext.GetDrawTarget()->CreateSimilarDrawTarget( + tileSize, gfx::SurfaceFormat::B8G8R8A8); + if (!tileTarget || !tileTarget->IsValid()) { + return false; + } + + { + gfxContext tileContext(tileTarget); + + tileContext.SetPattern(aGradientPattern); + tileContext.Paint(); + } + + tileSurface = tileTarget->Snapshot(); + tileTarget = nullptr; + } + + // Draw the gradient using tileSurface as a repeating pattern masked by + // the dirtyRect + Matrix tileTransform = Matrix::Translation( + NSAppUnitsToFloatPixels(aXStart, appUnitsPerDevPixel), + NSAppUnitsToFloatPixels(aYStart, appUnitsPerDevPixel)); + + aContext.NewPath(); + aContext.Rectangle(aDirtyAreaToFill); + aContext.Fill(SurfacePattern(tileSurface, ExtendMode::REPEAT, tileTransform)); + + return true; +} + +class MOZ_STACK_CLASS WrColorStopInterpolator + : public ColorStopInterpolator { + public: + WrColorStopInterpolator( + const nsTArray& aStops, + const StyleColorInterpolationMethod& aStyleColorInterpolationMethod, + float aOpacity, nsTArray& aResult) + : ColorStopInterpolator(aStops, aStyleColorInterpolationMethod), + mResult(aResult), + mOpacity(aOpacity), + mOutputStop(0) {} + + void CreateStops() { + mResult.SetLengthAndRetainStorage(0); + // we always emit at least two stops (start and end) for each input stop, + // which avoids ambiguity with incomplete oklch/lch/hsv/hsb color stops for + // the last stop pair, where the last color stop can't be interpreted on its + // own because it actually depends on the previous stop. + mResult.SetLength(mStops.Length() * 2 + kFullRangeExtraStops); + mOutputStop = 0; + ColorStopInterpolator::CreateStops(); + mResult.SetLength(mOutputStop); + } + + void CreateStop(float aPosition, DeviceColor aColor) { + if (mOutputStop < mResult.Capacity()) { + mResult[mOutputStop].color = wr::ToColorF(aColor); + mResult[mOutputStop].color.a *= mOpacity; + mResult[mOutputStop].offset = aPosition; + mOutputStop++; + } + } + + private: + nsTArray& mResult; + float mOpacity; + uint32_t mOutputStop; +}; + +void nsCSSGradientRenderer::BuildWebRenderParameters( + float aOpacity, wr::ExtendMode& aMode, nsTArray& aStops, + LayoutDevicePoint& aLineStart, LayoutDevicePoint& aLineEnd, + LayoutDeviceSize& aGradientRadius, LayoutDevicePoint& aGradientCenter, + float& aGradientAngle) { + aMode = + mGradient->Repeating() ? wr::ExtendMode::Repeat : wr::ExtendMode::Clamp; + + // If the interpolation space is not sRGB, or if color management is active, + // we need to add additional stops so that the sRGB interpolation in WebRender + // still closely approximates the correct curves. We prefer avoiding this if + // the gradient is simple because WebRender has fast rendering of linear + // gradients with 2 stops (which represent >99% of all gradients on the web). + // + // WebRender doesn't have easy access to StyleAbsoluteColor and CMS display + // color correction, so we just expand the gradient stop table significantly + // so that gamma and hue interpolation errors become imperceptible. + // + // This always turns into 128 pairs of stops inside WebRender as an + // implementation detail, so the number of stops we generate here should have + // very little impact on performance as the texture upload is always the same, + // except for the special linear gradient 2-stop case, and it is gpucache so + // if it does not change it is not re-uploaded. + // + // Color management bugs that this addresses: + // * https://bugzilla.mozilla.org/show_bug.cgi?id=939387 + // * https://bugzilla.mozilla.org/show_bug.cgi?id=1248178 + StyleColorInterpolationMethod styleColorInterpolationMethod = + mGradient->ColorInterpolationMethod(); + if (mStops.Length() >= 2 && + (styleColorInterpolationMethod.space != StyleColorSpace::Srgb || + gfxPlatform::GetCMSMode() == CMSMode::All)) { + WrColorStopInterpolator interpolator(mStops, styleColorInterpolationMethod, + aOpacity, aStops); + interpolator.CreateStops(); + } else { + aStops.SetLength(mStops.Length()); + for (uint32_t i = 0; i < mStops.Length(); i++) { + aStops[i].color = wr::ToColorF(ToDeviceColor(mStops[i].mColor)); + aStops[i].color.a *= aOpacity; + aStops[i].offset = (float)mStops[i].mPosition; + } + } + + aLineStart = LayoutDevicePoint(mLineStart.x, mLineStart.y); + aLineEnd = LayoutDevicePoint(mLineEnd.x, mLineEnd.y); + aGradientRadius = LayoutDeviceSize(mRadiusX, mRadiusY); + aGradientCenter = LayoutDevicePoint(mCenter.x, mCenter.y); + aGradientAngle = mAngle; +} + +void nsCSSGradientRenderer::BuildWebRenderDisplayItems( + wr::DisplayListBuilder& aBuilder, const layers::StackingContextHelper& aSc, + const nsRect& aDest, const nsRect& aFillArea, const nsSize& aRepeatSize, + const CSSIntRect& aSrc, bool aIsBackfaceVisible, float aOpacity) { + if (aDest.IsEmpty() || aFillArea.IsEmpty()) { + return; + } + + wr::ExtendMode extendMode; + nsTArray stops; + LayoutDevicePoint lineStart; + LayoutDevicePoint lineEnd; + LayoutDeviceSize gradientRadius; + LayoutDevicePoint gradientCenter; + float gradientAngle; + BuildWebRenderParameters(aOpacity, extendMode, stops, lineStart, lineEnd, + gradientRadius, gradientCenter, gradientAngle); + + nscoord appUnitsPerDevPixel = mPresContext->AppUnitsPerDevPixel(); + + nsPoint firstTile = + nsPoint(FindTileStart(aFillArea.x, aDest.x, aRepeatSize.width), + FindTileStart(aFillArea.y, aDest.y, aRepeatSize.height)); + + // Translate the parameters into device coordinates + LayoutDeviceRect clipBounds = + LayoutDevicePixel::FromAppUnits(aFillArea, appUnitsPerDevPixel); + LayoutDeviceRect firstTileBounds = LayoutDevicePixel::FromAppUnits( + nsRect(firstTile, aDest.Size()), appUnitsPerDevPixel); + LayoutDeviceSize tileRepeat = + LayoutDevicePixel::FromAppUnits(aRepeatSize, appUnitsPerDevPixel); + + // Calculate the bounds of the gradient display item, which starts at the + // first tile and extends to the end of clip bounds + LayoutDevicePoint tileToClip = + clipBounds.BottomRight() - firstTileBounds.TopLeft(); + LayoutDeviceRect gradientBounds = LayoutDeviceRect( + firstTileBounds.TopLeft(), LayoutDeviceSize(tileToClip.x, tileToClip.y)); + + // Calculate the tile spacing, which is the repeat size minus the tile size + LayoutDeviceSize tileSpacing = tileRepeat - firstTileBounds.Size(); + + // srcTransform is used for scaling the gradient to match aSrc + LayoutDeviceRect srcTransform = LayoutDeviceRect( + nsPresContext::CSSPixelsToAppUnits(aSrc.x), + nsPresContext::CSSPixelsToAppUnits(aSrc.y), + aDest.width / ((float)nsPresContext::CSSPixelsToAppUnits(aSrc.width)), + aDest.height / ((float)nsPresContext::CSSPixelsToAppUnits(aSrc.height))); + + lineStart.x = (lineStart.x - srcTransform.x) * srcTransform.width; + lineStart.y = (lineStart.y - srcTransform.y) * srcTransform.height; + + gradientCenter.x = (gradientCenter.x - srcTransform.x) * srcTransform.width; + gradientCenter.y = (gradientCenter.y - srcTransform.y) * srcTransform.height; + + if (mGradient->IsLinear()) { + lineEnd.x = (lineEnd.x - srcTransform.x) * srcTransform.width; + lineEnd.y = (lineEnd.y - srcTransform.y) * srcTransform.height; + + aBuilder.PushLinearGradient( + mozilla::wr::ToLayoutRect(gradientBounds), + mozilla::wr::ToLayoutRect(clipBounds), aIsBackfaceVisible, + mozilla::wr::ToLayoutPoint(lineStart), + mozilla::wr::ToLayoutPoint(lineEnd), stops, extendMode, + mozilla::wr::ToLayoutSize(firstTileBounds.Size()), + mozilla::wr::ToLayoutSize(tileSpacing)); + } else if (mGradient->IsRadial()) { + gradientRadius.width *= srcTransform.width; + gradientRadius.height *= srcTransform.height; + + aBuilder.PushRadialGradient( + mozilla::wr::ToLayoutRect(gradientBounds), + mozilla::wr::ToLayoutRect(clipBounds), aIsBackfaceVisible, + mozilla::wr::ToLayoutPoint(lineStart), + mozilla::wr::ToLayoutSize(gradientRadius), stops, extendMode, + mozilla::wr::ToLayoutSize(firstTileBounds.Size()), + mozilla::wr::ToLayoutSize(tileSpacing)); + } else { + MOZ_ASSERT(mGradient->IsConic()); + aBuilder.PushConicGradient( + mozilla::wr::ToLayoutRect(gradientBounds), + mozilla::wr::ToLayoutRect(clipBounds), aIsBackfaceVisible, + mozilla::wr::ToLayoutPoint(gradientCenter), gradientAngle, stops, + extendMode, mozilla::wr::ToLayoutSize(firstTileBounds.Size()), + mozilla::wr::ToLayoutSize(tileSpacing)); + } +} + +} // namespace mozilla -- cgit v1.2.3