mirror of
https://github.com/servo/servo.git
synced 2025-06-04 07:35:36 +00:00
* Use 2024 style edition Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> * Reformat all code Signed-off-by: Simon Wülker <simon.wuelker@arcor.de> --------- Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
468 lines
18 KiB
Rust
468 lines
18 KiB
Rust
/* 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 https://mozilla.org/MPL/2.0/. */
|
||
|
||
use app_units::Au;
|
||
use euclid::Size2D;
|
||
use style::Zero;
|
||
use style::color::mix::ColorInterpolationMethod;
|
||
use style::properties::ComputedValues;
|
||
use style::values::computed::image::{EndingShape, Gradient, LineDirection};
|
||
use style::values::computed::{Angle, AngleOrPercentage, Color, LengthPercentage, Position};
|
||
use style::values::generics::image::{
|
||
Circle, ColorStop, Ellipse, GradientFlags, GradientItem, ShapeExtent,
|
||
};
|
||
use webrender_api::units::LayoutPixel;
|
||
use webrender_api::{
|
||
self as wr, ConicGradient as WebRenderConicGradient, Gradient as WebRenderLinearGradient,
|
||
RadialGradient as WebRenderRadialGradient, units,
|
||
};
|
||
use wr::ColorF;
|
||
|
||
pub(super) enum WebRenderGradient {
|
||
Linear(WebRenderLinearGradient),
|
||
Radial(WebRenderRadialGradient),
|
||
Conic(WebRenderConicGradient),
|
||
}
|
||
|
||
pub(super) fn build(
|
||
style: &ComputedValues,
|
||
gradient: &Gradient,
|
||
size: Size2D<f32, LayoutPixel>,
|
||
builder: &mut super::DisplayListBuilder,
|
||
) -> WebRenderGradient {
|
||
match gradient {
|
||
Gradient::Linear {
|
||
ref items,
|
||
ref direction,
|
||
ref color_interpolation_method,
|
||
ref flags,
|
||
compat_mode: _,
|
||
} => build_linear(
|
||
style,
|
||
items,
|
||
direction,
|
||
color_interpolation_method,
|
||
*flags,
|
||
size,
|
||
builder,
|
||
),
|
||
Gradient::Radial {
|
||
ref shape,
|
||
ref position,
|
||
ref color_interpolation_method,
|
||
ref items,
|
||
ref flags,
|
||
compat_mode: _,
|
||
} => build_radial(
|
||
style,
|
||
items,
|
||
shape,
|
||
position,
|
||
color_interpolation_method,
|
||
*flags,
|
||
size,
|
||
builder,
|
||
),
|
||
Gradient::Conic {
|
||
angle,
|
||
position,
|
||
color_interpolation_method,
|
||
items,
|
||
flags,
|
||
} => build_conic(
|
||
style,
|
||
*angle,
|
||
position,
|
||
*color_interpolation_method,
|
||
items,
|
||
*flags,
|
||
size,
|
||
builder,
|
||
),
|
||
}
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/css-images-3/#linear-gradients>
|
||
pub(super) fn build_linear(
|
||
style: &ComputedValues,
|
||
items: &[GradientItem<Color, LengthPercentage>],
|
||
line_direction: &LineDirection,
|
||
_color_interpolation_method: &ColorInterpolationMethod,
|
||
flags: GradientFlags,
|
||
gradient_box: Size2D<f32, LayoutPixel>,
|
||
builder: &mut super::DisplayListBuilder,
|
||
) -> WebRenderGradient {
|
||
use style::values::specified::position::HorizontalPositionKeyword::*;
|
||
use style::values::specified::position::VerticalPositionKeyword::*;
|
||
use units::LayoutVector2D as Vec2;
|
||
|
||
// A vector of length 1.0 in the direction of the gradient line
|
||
let direction = match line_direction {
|
||
LineDirection::Horizontal(Right) => Vec2::new(1., 0.),
|
||
LineDirection::Vertical(Top) => Vec2::new(0., -1.),
|
||
LineDirection::Horizontal(Left) => Vec2::new(-1., 0.),
|
||
LineDirection::Vertical(Bottom) => Vec2::new(0., 1.),
|
||
|
||
LineDirection::Angle(angle) => {
|
||
let radians = angle.radians();
|
||
// “`0deg` points upward,
|
||
// and positive angles represent clockwise rotation,
|
||
// so `90deg` point toward the right.”
|
||
Vec2::new(radians.sin(), -radians.cos())
|
||
},
|
||
|
||
LineDirection::Corner(horizontal, vertical) => {
|
||
// “If the argument instead specifies a corner of the box such as `to top left`,
|
||
// the gradient line must be angled such that it points
|
||
// into the same quadrant as the specified corner,
|
||
// and is perpendicular to a line intersecting
|
||
// the two neighboring corners of the gradient box.”
|
||
|
||
// Note that that last line is a diagonal of the gradient box rectangle,
|
||
// since two neighboring corners of a third corner
|
||
// are necessarily opposite to each other.
|
||
|
||
// `{ x: gradient_box.width, y: gradient_box.height }` is such a diagonal vector,
|
||
// from the bottom left corner to the top right corner of the gradient box.
|
||
// (Both coordinates are positive.)
|
||
// Changing either or both signs produces the other three (oriented) diagonals.
|
||
|
||
// Swapping the coordinates `{ x: gradient_box.height, y: gradient_box.height }`
|
||
// produces a vector perpendicular to some diagonal of the rectangle.
|
||
// Finally, we choose the sign of each cartesian coordinate
|
||
// such that our vector points to the desired quadrant.
|
||
|
||
let x = match horizontal {
|
||
Right => gradient_box.height,
|
||
Left => -gradient_box.height,
|
||
};
|
||
let y = match vertical {
|
||
Top => gradient_box.width,
|
||
Bottom => -gradient_box.width,
|
||
};
|
||
|
||
// `{ x, y }` is now a vector of arbitrary length
|
||
// with the same direction as the gradient line.
|
||
// This normalizes the length to 1.0:
|
||
Vec2::new(x, y).normalize()
|
||
},
|
||
};
|
||
|
||
// This formula is given as `abs(W * sin(A)) + abs(H * cos(A))` in a note in the spec, under
|
||
// https://drafts.csswg.org/css-images-3/#linear-gradient-syntax
|
||
//
|
||
// Sketch of a proof:
|
||
//
|
||
// * Take the top side of the gradient box rectangle. It is a segment of length `W`
|
||
// * Project onto the gradient line. You get a segment of length `abs(W * sin(A))`
|
||
// * Similarly, the left side of the rectangle (length `H`)
|
||
// projects to a segment of length `abs(H * cos(A))`
|
||
// * These two segments add up to exactly the gradient line.
|
||
//
|
||
// See the illustration in the example under
|
||
// https://drafts.csswg.org/css-images-3/#linear-gradient-syntax
|
||
let gradient_line_length =
|
||
(gradient_box.width * direction.x).abs() + (gradient_box.height * direction.y).abs();
|
||
|
||
let half_gradient_line = direction * (gradient_line_length / 2.);
|
||
let center = (gradient_box / 2.).to_vector().to_point();
|
||
let start_point = center - half_gradient_line;
|
||
let end_point = center + half_gradient_line;
|
||
|
||
let mut color_stops =
|
||
gradient_items_to_color_stops(style, items, Au::from_f32_px(gradient_line_length));
|
||
let stops = fixup_stops(&mut color_stops);
|
||
let extend_mode = if flags.contains(GradientFlags::REPEATING) {
|
||
wr::ExtendMode::Repeat
|
||
} else {
|
||
wr::ExtendMode::Clamp
|
||
};
|
||
WebRenderGradient::Linear(builder.wr().create_gradient(
|
||
start_point,
|
||
end_point,
|
||
stops,
|
||
extend_mode,
|
||
))
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/css-images-3/#radial-gradients>
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub(super) fn build_radial(
|
||
style: &ComputedValues,
|
||
items: &[GradientItem<Color, LengthPercentage>],
|
||
shape: &EndingShape,
|
||
center: &Position,
|
||
_color_interpolation_method: &ColorInterpolationMethod,
|
||
flags: GradientFlags,
|
||
gradient_box: Size2D<f32, LayoutPixel>,
|
||
builder: &mut super::DisplayListBuilder,
|
||
) -> WebRenderGradient {
|
||
let center = units::LayoutPoint::new(
|
||
center
|
||
.horizontal
|
||
.to_used_value(Au::from_f32_px(gradient_box.width))
|
||
.to_f32_px(),
|
||
center
|
||
.vertical
|
||
.to_used_value(Au::from_f32_px(gradient_box.height))
|
||
.to_f32_px(),
|
||
);
|
||
let radii = match shape {
|
||
EndingShape::Circle(circle) => {
|
||
let radius = match circle {
|
||
Circle::Radius(r) => r.0.px(),
|
||
Circle::Extent(extent) => match extent {
|
||
ShapeExtent::ClosestSide | ShapeExtent::Contain => {
|
||
let vec = abs_vector_to_corner(gradient_box, center, f32::min);
|
||
vec.x.min(vec.y)
|
||
},
|
||
ShapeExtent::FarthestSide => {
|
||
let vec = abs_vector_to_corner(gradient_box, center, f32::max);
|
||
vec.x.max(vec.y)
|
||
},
|
||
ShapeExtent::ClosestCorner => {
|
||
abs_vector_to_corner(gradient_box, center, f32::min).length()
|
||
},
|
||
ShapeExtent::FarthestCorner | ShapeExtent::Cover => {
|
||
abs_vector_to_corner(gradient_box, center, f32::max).length()
|
||
},
|
||
},
|
||
};
|
||
units::LayoutSize::new(radius, radius)
|
||
},
|
||
EndingShape::Ellipse(Ellipse::Radii(rx, ry)) => units::LayoutSize::new(
|
||
rx.0.to_used_value(Au::from_f32_px(gradient_box.width))
|
||
.to_f32_px(),
|
||
ry.0.to_used_value(Au::from_f32_px(gradient_box.height))
|
||
.to_f32_px(),
|
||
),
|
||
EndingShape::Ellipse(Ellipse::Extent(extent)) => match extent {
|
||
ShapeExtent::ClosestSide | ShapeExtent::Contain => {
|
||
abs_vector_to_corner(gradient_box, center, f32::min).to_size()
|
||
},
|
||
ShapeExtent::FarthestSide => {
|
||
abs_vector_to_corner(gradient_box, center, f32::max).to_size()
|
||
},
|
||
ShapeExtent::ClosestCorner => {
|
||
abs_vector_to_corner(gradient_box, center, f32::min).to_size() *
|
||
(std::f32::consts::FRAC_1_SQRT_2 * 2.0)
|
||
},
|
||
ShapeExtent::FarthestCorner | ShapeExtent::Cover => {
|
||
abs_vector_to_corner(gradient_box, center, f32::max).to_size() *
|
||
(std::f32::consts::FRAC_1_SQRT_2 * 2.0)
|
||
},
|
||
},
|
||
};
|
||
|
||
/// Returns the distance to the nearest or farthest sides in the respective dimension,
|
||
/// depending on `select`.
|
||
fn abs_vector_to_corner(
|
||
gradient_box: units::LayoutSize,
|
||
center: units::LayoutPoint,
|
||
select: impl Fn(f32, f32) -> f32,
|
||
) -> units::LayoutVector2D {
|
||
let left = center.x.abs();
|
||
let top = center.y.abs();
|
||
let right = (gradient_box.width - center.x).abs();
|
||
let bottom = (gradient_box.height - center.y).abs();
|
||
units::LayoutVector2D::new(select(left, right), select(top, bottom))
|
||
}
|
||
|
||
// “The gradient line’s starting point is at the center of the gradient,
|
||
// and it extends toward the right, with the ending point on the point
|
||
// where the gradient line intersects the ending shape.”
|
||
let gradient_line_length = radii.width;
|
||
|
||
let mut color_stops =
|
||
gradient_items_to_color_stops(style, items, Au::from_f32_px(gradient_line_length));
|
||
let stops = fixup_stops(&mut color_stops);
|
||
let extend_mode = if flags.contains(GradientFlags::REPEATING) {
|
||
wr::ExtendMode::Repeat
|
||
} else {
|
||
wr::ExtendMode::Clamp
|
||
};
|
||
WebRenderGradient::Radial(builder.wr().create_radial_gradient(
|
||
center,
|
||
radii,
|
||
stops,
|
||
extend_mode,
|
||
))
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/css-images-4/#conic-gradients>
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn build_conic(
|
||
style: &ComputedValues,
|
||
angle: Angle,
|
||
center: &Position,
|
||
_color_interpolation_method: ColorInterpolationMethod,
|
||
items: &[GradientItem<Color, AngleOrPercentage>],
|
||
flags: GradientFlags,
|
||
gradient_box: Size2D<f32, LayoutPixel>,
|
||
builder: &mut super::DisplayListBuilder<'_>,
|
||
) -> WebRenderGradient {
|
||
let center = units::LayoutPoint::new(
|
||
center
|
||
.horizontal
|
||
.to_used_value(Au::from_f32_px(gradient_box.width))
|
||
.to_f32_px(),
|
||
center
|
||
.vertical
|
||
.to_used_value(Au::from_f32_px(gradient_box.height))
|
||
.to_f32_px(),
|
||
);
|
||
let mut color_stops = conic_gradient_items_to_color_stops(style, items);
|
||
let stops = fixup_stops(&mut color_stops);
|
||
let extend_mode = if flags.contains(GradientFlags::REPEATING) {
|
||
wr::ExtendMode::Repeat
|
||
} else {
|
||
wr::ExtendMode::Clamp
|
||
};
|
||
WebRenderGradient::Conic(builder.wr().create_conic_gradient(
|
||
center,
|
||
angle.radians(),
|
||
stops,
|
||
extend_mode,
|
||
))
|
||
}
|
||
|
||
fn conic_gradient_items_to_color_stops(
|
||
style: &ComputedValues,
|
||
items: &[GradientItem<Color, AngleOrPercentage>],
|
||
) -> Vec<ColorStop<ColorF, f32>> {
|
||
// Remove color transititon hints, which are not supported yet.
|
||
// https://drafts.csswg.org/css-images-4/#color-transition-hint
|
||
//
|
||
// This gives an approximation of the gradient that might be visibly wrong,
|
||
// but maybe better than not parsing that value at all?
|
||
// It’s debatble whether that’s better or worse
|
||
// than not parsing and allowing authors to set a fallback.
|
||
// Either way, the best outcome is to add support.
|
||
// Gecko does so by approximating the non-linear interpolation
|
||
// by up to 10 piece-wise linear segments (9 intermediate color stops)
|
||
items
|
||
.iter()
|
||
.filter_map(|item| {
|
||
match item {
|
||
GradientItem::SimpleColorStop(color) => Some(ColorStop {
|
||
color: super::rgba(style.resolve_color(color)),
|
||
position: None,
|
||
}),
|
||
GradientItem::ComplexColorStop { color, position } => Some(ColorStop {
|
||
color: super::rgba(style.resolve_color(color)),
|
||
position: match position {
|
||
AngleOrPercentage::Percentage(percentage) => Some(percentage.0),
|
||
AngleOrPercentage::Angle(angle) => Some(angle.degrees() / 360.),
|
||
},
|
||
}),
|
||
// FIXME: approximate like in:
|
||
// https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391
|
||
GradientItem::InterpolationHint(_) => None,
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn gradient_items_to_color_stops(
|
||
style: &ComputedValues,
|
||
items: &[GradientItem<Color, LengthPercentage>],
|
||
gradient_line_length: Au,
|
||
) -> Vec<ColorStop<ColorF, f32>> {
|
||
// Remove color transititon hints, which are not supported yet.
|
||
// https://drafts.csswg.org/css-images-4/#color-transition-hint
|
||
//
|
||
// This gives an approximation of the gradient that might be visibly wrong,
|
||
// but maybe better than not parsing that value at all?
|
||
// It’s debatble whether that’s better or worse
|
||
// than not parsing and allowing authors to set a fallback.
|
||
// Either way, the best outcome is to add support.
|
||
// Gecko does so by approximating the non-linear interpolation
|
||
// by up to 10 piece-wise linear segments (9 intermediate color stops)
|
||
items
|
||
.iter()
|
||
.filter_map(|item| {
|
||
match item {
|
||
GradientItem::SimpleColorStop(color) => Some(ColorStop {
|
||
color: super::rgba(style.resolve_color(color)),
|
||
position: None,
|
||
}),
|
||
GradientItem::ComplexColorStop { color, position } => Some(ColorStop {
|
||
color: super::rgba(style.resolve_color(color)),
|
||
position: Some(if gradient_line_length.is_zero() {
|
||
0.
|
||
} else {
|
||
position
|
||
.to_used_value(gradient_line_length)
|
||
.scale_by(1. / gradient_line_length.to_f32_px())
|
||
.to_f32_px()
|
||
}),
|
||
}),
|
||
// FIXME: approximate like in:
|
||
// https://searchfox.org/mozilla-central/rev/f98dad153b59a985efd4505912588d4651033395/layout/painting/nsCSSRenderingGradients.cpp#315-391
|
||
GradientItem::InterpolationHint(_) => None,
|
||
}
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// <https://drafts.csswg.org/css-images-4/#color-stop-fixup>
|
||
fn fixup_stops(stops: &mut [ColorStop<ColorF, f32>]) -> Vec<wr::GradientStop> {
|
||
assert!(!stops.is_empty());
|
||
|
||
// https://drafts.csswg.org/css-images-4/#color-stop-fixup
|
||
if let first_position @ None = &mut stops.first_mut().unwrap().position {
|
||
*first_position = Some(0.);
|
||
}
|
||
if let last_position @ None = &mut stops.last_mut().unwrap().position {
|
||
*last_position = Some(1.);
|
||
}
|
||
|
||
let mut iter = stops.iter_mut();
|
||
let mut max_so_far = iter.next().unwrap().position.unwrap();
|
||
for stop in iter {
|
||
if let Some(position) = &mut stop.position {
|
||
if *position < max_so_far {
|
||
*position = max_so_far
|
||
} else {
|
||
max_so_far = *position
|
||
}
|
||
}
|
||
}
|
||
|
||
let mut wr_stops = Vec::with_capacity(stops.len());
|
||
let mut iter = stops.iter().enumerate();
|
||
let (_, first) = iter.next().unwrap();
|
||
let first_stop_position = first.position.unwrap();
|
||
wr_stops.push(wr::GradientStop {
|
||
offset: first_stop_position,
|
||
color: first.color,
|
||
});
|
||
if stops.len() == 1 {
|
||
wr_stops.push(wr_stops[0]);
|
||
}
|
||
|
||
let mut last_positioned_stop_index = 0;
|
||
let mut last_positioned_stop_position = first_stop_position;
|
||
for (i, stop) in iter {
|
||
if let Some(position) = stop.position {
|
||
let step_count = i - last_positioned_stop_index;
|
||
if step_count > 1 {
|
||
let step = (position - last_positioned_stop_position) / step_count as f32;
|
||
for j in 1..step_count {
|
||
let color = stops[last_positioned_stop_index + j].color;
|
||
let offset = last_positioned_stop_position + j as f32 * step;
|
||
wr_stops.push(wr::GradientStop { offset, color })
|
||
}
|
||
}
|
||
last_positioned_stop_index = i;
|
||
last_positioned_stop_position = position;
|
||
wr_stops.push(wr::GradientStop {
|
||
offset: position,
|
||
color: stop.color,
|
||
})
|
||
}
|
||
}
|
||
|
||
wr_stops
|
||
}
|