Split layout/background.rs file

Have background, border and gradient modules
for calculation functions.
Use shorter names for functions that are qualified
by the module name like `border::radii`.

Use push_item and push_iter to add items to WebRender.
This commit is contained in:
Pyfisch 2018-10-17 11:06:50 +02:00
parent 2304f02123
commit 2ff330a5c9
7 changed files with 577 additions and 658 deletions

View file

@ -2,52 +2,23 @@
* 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/. */
//! Calculations for CSS images, backgrounds and borders.
//!
//! * [CSS Images Module Level 3](https://drafts.csswg.org/css-images-3/)
//! * [CSS Backgrounds and Borders Module Level 3](https://drafts.csswg.org/css-backgrounds-3/)
#![deny(unsafe_code)]
// FIXME(rust-lang/rust#26264): Remove GenericEndingShape, GenericGradientItem,
// GenericBackgroundSize and GenericBorderImageSideWidth.
// FIXME(rust-lang/rust#26264): Remove GenericBackgroundSize.
use app_units::Au;
use display_list::ToLayout;
use euclid::{Point2D, Rect, SideOffsets2D, Size2D, Vector2D};
use model::{self, MaybeAuto};
use display_list::border;
use euclid::{Point2D, Rect, SideOffsets2D, Size2D};
use model::MaybeAuto;
use style::computed_values::background_attachment::single_value::T as BackgroundAttachment;
use style::computed_values::background_clip::single_value::T as BackgroundClip;
use style::computed_values::background_origin::single_value::T as BackgroundOrigin;
use style::computed_values::border_image_outset::T as BorderImageOutset;
use style::properties::ComputedValues;
use style::properties::style_structs::{self, Background};
use style::values::Either;
use style::values::computed::{Angle, GradientItem, BackgroundSize};
use style::values::computed::{BorderImageWidth, BorderImageSideWidth};
use style::values::computed::{LengthOrNumber, LengthOrPercentage, LengthOrPercentageOrAuto};
use style::values::computed::{NumberOrPercentage, Percentage, Position};
use style::values::computed::image::{EndingShape, LineDirection};
use style::properties::style_structs::Background;
use style::values::computed::{BackgroundSize, LengthOrPercentageOrAuto};
use style::values::generics::NonNegative;
use style::values::generics::background::BackgroundSize as GenericBackgroundSize;
use style::values::generics::border::{BorderImageSideWidth as GenericBorderImageSideWidth};
use style::values::generics::image::{Circle, Ellipse, ShapeExtent};
use style::values::generics::image::EndingShape as GenericEndingShape;
use style::values::generics::image::GradientItem as GenericGradientItem;
use style::values::specified::background::BackgroundRepeatKeyword;
use style::values::specified::position::{X, Y};
use webrender_api::{BorderRadius, BorderSide, BorderStyle, ColorF};
use webrender_api::{ExtendMode, Gradient, GradientStop, LayoutSize, LayoutSideOffsets};
use webrender_api::{NormalBorder, RadialGradient};
/// A helper data structure for gradients.
#[derive(Clone, Copy)]
struct StopRun {
start_offset: f32,
end_offset: f32,
start_index: usize,
stop_count: usize,
}
use webrender_api::BorderRadius;
/// Placment information for both image and gradient backgrounds.
#[derive(Clone, Copy, Debug)]
@ -70,19 +41,6 @@ pub struct BackgroundPlacement {
pub fixed: bool,
}
pub trait ResolvePercentage {
fn resolve(&self, length: u32) -> u32;
}
impl ResolvePercentage for NumberOrPercentage {
fn resolve(&self, length: u32) -> u32 {
match *self {
NumberOrPercentage::Percentage(p) => (p.0 * length as f32).round() as u32,
NumberOrPercentage::Number(n) => n.round() as u32,
}
}
}
/// Access element at index modulo the array length.
///
/// Obviously it does not work with empty arrays.
@ -162,7 +120,7 @@ fn compute_background_image_size(
}
}
pub fn compute_background_clip(
pub fn clip(
bg_clip: BackgroundClip,
absolute_bounds: Rect<Au>,
border: SideOffsets2D<Au>,
@ -173,11 +131,11 @@ pub fn compute_background_clip(
BackgroundClip::BorderBox => (absolute_bounds, border_radii),
BackgroundClip::PaddingBox => (
absolute_bounds.inner_rect(border),
calculate_inner_border_radii(border_radii, border),
border::inner_radii(border_radii, border),
),
BackgroundClip::ContentBox => (
absolute_bounds.inner_rect(border_padding),
calculate_inner_border_radii(border_radii, border_padding),
border::inner_radii(border_radii, border_padding),
),
}
}
@ -186,7 +144,7 @@ pub fn compute_background_clip(
///
/// Photos have their resolution as intrinsic size while gradients have
/// no intrinsic size.
pub fn compute_background_placement(
pub fn placement(
bg: &Background,
viewport_size: Size2D<Au>,
absolute_bounds: Rect<Au>,
@ -204,7 +162,7 @@ pub fn compute_background_placement(
let bg_repeat = get_cyclic(&bg.background_repeat.0, index);
let bg_size = *get_cyclic(&bg.background_size.0, index);
let (clip_rect, clip_radii) = compute_background_clip(
let (clip_rect, clip_radii) = clip(
bg_clip,
absolute_bounds,
border,
@ -380,468 +338,3 @@ fn tile_image_axis(
},
}
}
/// Determines the radius of a circle if it was not explictly provided.
/// <https://drafts.csswg.org/css-images-3/#typedef-size>
fn convert_circle_size_keyword(
keyword: ShapeExtent,
size: &Size2D<Au>,
center: &Point2D<Au>,
) -> Size2D<Au> {
let radius = match keyword {
ShapeExtent::ClosestSide | ShapeExtent::Contain => {
let dist = get_distance_to_sides(size, center, ::std::cmp::min);
::std::cmp::min(dist.width, dist.height)
},
ShapeExtent::FarthestSide => {
let dist = get_distance_to_sides(size, center, ::std::cmp::max);
::std::cmp::max(dist.width, dist.height)
},
ShapeExtent::ClosestCorner => get_distance_to_corner(size, center, ::std::cmp::min),
ShapeExtent::FarthestCorner | ShapeExtent::Cover => {
get_distance_to_corner(size, center, ::std::cmp::max)
},
};
Size2D::new(radius, radius)
}
/// Returns the radius for an ellipse with the same ratio as if it was matched to the sides.
fn get_ellipse_radius<F>(size: &Size2D<Au>, center: &Point2D<Au>, cmp: F) -> Size2D<Au>
where
F: Fn(Au, Au) -> Au,
{
let dist = get_distance_to_sides(size, center, cmp);
Size2D::new(
dist.width.scale_by(::std::f32::consts::FRAC_1_SQRT_2 * 2.0),
dist.height
.scale_by(::std::f32::consts::FRAC_1_SQRT_2 * 2.0),
)
}
/// Determines the radius of an ellipse if it was not explictly provided.
/// <https://drafts.csswg.org/css-images-3/#typedef-size>
fn convert_ellipse_size_keyword(
keyword: ShapeExtent,
size: &Size2D<Au>,
center: &Point2D<Au>,
) -> Size2D<Au> {
match keyword {
ShapeExtent::ClosestSide | ShapeExtent::Contain => {
get_distance_to_sides(size, center, ::std::cmp::min)
},
ShapeExtent::FarthestSide => get_distance_to_sides(size, center, ::std::cmp::max),
ShapeExtent::ClosestCorner => get_ellipse_radius(size, center, ::std::cmp::min),
ShapeExtent::FarthestCorner | ShapeExtent::Cover => {
get_ellipse_radius(size, center, ::std::cmp::max)
},
}
}
fn convert_gradient_stops(
style: &ComputedValues,
gradient_items: &[GradientItem],
total_length: Au,
) -> Vec<GradientStop> {
// Determine the position of each stop per CSS-IMAGES § 3.4.
// Only keep the color stops, discard the color interpolation hints.
let mut stop_items = gradient_items
.iter()
.filter_map(|item| match *item {
GenericGradientItem::ColorStop(ref stop) => Some(*stop),
_ => None,
}).collect::<Vec<_>>();
assert!(stop_items.len() >= 2);
// Run the algorithm from
// https://drafts.csswg.org/css-images-3/#color-stop-syntax
// Step 1:
// If the first color stop does not have a position, set its position to 0%.
{
let first = stop_items.first_mut().unwrap();
if first.position.is_none() {
first.position = Some(LengthOrPercentage::Percentage(Percentage(0.0)));
}
}
// If the last color stop does not have a position, set its position to 100%.
{
let last = stop_items.last_mut().unwrap();
if last.position.is_none() {
last.position = Some(LengthOrPercentage::Percentage(Percentage(1.0)));
}
}
// Step 2: Move any stops placed before earlier stops to the
// same position as the preceding stop.
let mut last_stop_position = stop_items.first().unwrap().position.unwrap();
for stop in stop_items.iter_mut().skip(1) {
if let Some(pos) = stop.position {
if position_to_offset(last_stop_position, total_length) >
position_to_offset(pos, total_length)
{
stop.position = Some(last_stop_position);
}
last_stop_position = stop.position.unwrap();
}
}
// Step 3: Evenly space stops without position.
let mut stops = Vec::with_capacity(stop_items.len());
let mut stop_run = None;
for (i, stop) in stop_items.iter().enumerate() {
let offset = match stop.position {
None => {
if stop_run.is_none() {
// Initialize a new stop run.
// `unwrap()` here should never fail because this is the beginning of
// a stop run, which is always bounded by a length or percentage.
let start_offset =
position_to_offset(stop_items[i - 1].position.unwrap(), total_length);
// `unwrap()` here should never fail because this is the end of
// a stop run, which is always bounded by a length or percentage.
let (end_index, end_stop) = stop_items[(i + 1)..]
.iter()
.enumerate()
.find(|&(_, ref stop)| stop.position.is_some())
.unwrap();
let end_offset = position_to_offset(end_stop.position.unwrap(), total_length);
stop_run = Some(StopRun {
start_offset,
end_offset,
start_index: i - 1,
stop_count: end_index,
})
}
let stop_run = stop_run.unwrap();
let stop_run_length = stop_run.end_offset - stop_run.start_offset;
stop_run.start_offset +
stop_run_length * (i - stop_run.start_index) as f32 /
((2 + stop_run.stop_count) as f32)
},
Some(position) => {
stop_run = None;
position_to_offset(position, total_length)
},
};
assert!(offset.is_finite());
stops.push(GradientStop {
offset: offset,
color: style.resolve_color(stop.color).to_layout(),
})
}
stops
}
fn as_gradient_extend_mode(repeating: bool) -> ExtendMode {
if repeating {
ExtendMode::Repeat
} else {
ExtendMode::Clamp
}
}
pub fn convert_linear_gradient(
style: &ComputedValues,
size: Size2D<Au>,
stops: &[GradientItem],
direction: LineDirection,
repeating: bool,
) -> (Gradient, Vec<GradientStop>) {
let angle = match direction {
LineDirection::Angle(angle) => angle.radians(),
LineDirection::Horizontal(x) => match x {
X::Left => Angle::from_degrees(270.).radians(),
X::Right => Angle::from_degrees(90.).radians(),
},
LineDirection::Vertical(y) => match y {
Y::Top => Angle::from_degrees(0.).radians(),
Y::Bottom => Angle::from_degrees(180.).radians(),
},
LineDirection::Corner(horizontal, vertical) => {
// This the angle for one of the diagonals of the box. Our angle
// will either be this one, this one + PI, or one of the other
// two perpendicular angles.
let atan = (size.height.to_f32_px() / size.width.to_f32_px()).atan();
match (horizontal, vertical) {
(X::Right, Y::Bottom) => ::std::f32::consts::PI - atan,
(X::Left, Y::Bottom) => ::std::f32::consts::PI + atan,
(X::Right, Y::Top) => atan,
(X::Left, Y::Top) => -atan,
}
},
};
// Get correct gradient line length, based on:
// https://drafts.csswg.org/css-images-3/#linear-gradients
let dir = Point2D::new(angle.sin(), -angle.cos());
let line_length =
(dir.x * size.width.to_f32_px()).abs() + (dir.y * size.height.to_f32_px()).abs();
let inv_dir_length = 1.0 / (dir.x * dir.x + dir.y * dir.y).sqrt();
// This is the vector between the center and the ending point; i.e. half
// of the distance between the starting point and the ending point.
let delta = Vector2D::new(
Au::from_f32_px(dir.x * inv_dir_length * line_length / 2.0),
Au::from_f32_px(dir.y * inv_dir_length * line_length / 2.0),
);
// This is the length of the gradient line.
let length = Au::from_f32_px((delta.x.to_f32_px() * 2.0).hypot(delta.y.to_f32_px() * 2.0));
let stops = convert_gradient_stops(style, stops, length);
let center = Point2D::new(size.width / 2, size.height / 2);
(
Gradient {
start_point: (center - delta).to_layout(),
end_point: (center + delta).to_layout(),
extend_mode: as_gradient_extend_mode(repeating),
},
stops,
)
}
pub fn convert_radial_gradient(
style: &ComputedValues,
size: Size2D<Au>,
stops: &[GradientItem],
shape: EndingShape,
center: Position,
repeating: bool,
) -> (RadialGradient, Vec<GradientStop>) {
let center = Point2D::new(
center.horizontal.to_used_value(size.width),
center.vertical.to_used_value(size.height),
);
let radius = match shape {
GenericEndingShape::Circle(Circle::Radius(length)) => {
let length = Au::from(length);
Size2D::new(length, length)
},
GenericEndingShape::Circle(Circle::Extent(extent)) => {
convert_circle_size_keyword(extent, &size, &center)
},
GenericEndingShape::Ellipse(Ellipse::Radii(x, y)) => {
Size2D::new(x.to_used_value(size.width), y.to_used_value(size.height))
},
GenericEndingShape::Ellipse(Ellipse::Extent(extent)) => {
convert_ellipse_size_keyword(extent, &size, &center)
},
};
let stops = convert_gradient_stops(style, stops, radius.width);
(
RadialGradient {
center: center.to_layout(),
radius: radius.to_layout(),
extend_mode: as_gradient_extend_mode(repeating),
// FIXME(pyfisch): These values are calculated by WR.
start_offset: 0.0,
end_offset: 0.0,
},
stops,
)
}
/// Returns the the distance to the nearest or farthest corner depending on the comperator.
fn get_distance_to_corner<F>(size: &Size2D<Au>, center: &Point2D<Au>, cmp: F) -> Au
where
F: Fn(Au, Au) -> Au,
{
let dist = get_distance_to_sides(size, center, cmp);
Au::from_f32_px(dist.width.to_f32_px().hypot(dist.height.to_f32_px()))
}
/// Returns the distance to the nearest or farthest sides depending on the comparator.
///
/// The first return value is horizontal distance the second vertical distance.
fn get_distance_to_sides<F>(size: &Size2D<Au>, center: &Point2D<Au>, cmp: F) -> Size2D<Au>
where
F: Fn(Au, Au) -> Au,
{
let top_side = center.y;
let right_side = size.width - center.x;
let bottom_side = size.height - center.y;
let left_side = center.x;
Size2D::new(cmp(left_side, right_side), cmp(top_side, bottom_side))
}
fn position_to_offset(position: LengthOrPercentage, total_length: Au) -> f32 {
if total_length == Au(0) {
return 0.0;
}
match position {
LengthOrPercentage::Length(l) => l.to_i32_au() as f32 / total_length.0 as f32,
LengthOrPercentage::Percentage(percentage) => percentage.0 as f32,
LengthOrPercentage::Calc(calc) => {
calc.to_used_value(Some(total_length)).unwrap().0 as f32 / total_length.0 as f32
},
}
}
fn scale_border_radii(radii: BorderRadius, factor: f32) -> BorderRadius {
BorderRadius {
top_left: radii.top_left * factor,
top_right: radii.top_right * factor,
bottom_left: radii.bottom_left * factor,
bottom_right: radii.bottom_right * factor,
}
}
fn handle_overlapping_radii(size: LayoutSize, radii: BorderRadius) -> BorderRadius {
// No two corners' border radii may add up to more than the length of the edge
// between them. To prevent that, all radii are scaled down uniformly.
fn scale_factor(radius_a: f32, radius_b: f32, edge_length: f32) -> f32 {
let required = radius_a + radius_b;
if required <= edge_length {
1.0
} else {
edge_length / required
}
}
let top_factor = scale_factor(radii.top_left.width, radii.top_right.width, size.width);
let bottom_factor = scale_factor(
radii.bottom_left.width,
radii.bottom_right.width,
size.width,
);
let left_factor = scale_factor(radii.top_left.height, radii.bottom_left.height, size.height);
let right_factor = scale_factor(
radii.top_right.height,
radii.bottom_right.height,
size.height,
);
let min_factor = top_factor
.min(bottom_factor)
.min(left_factor)
.min(right_factor);
if min_factor < 1.0 {
scale_border_radii(radii, min_factor)
} else {
radii
}
}
pub fn build_border_radius(
abs_bounds: Rect<Au>,
border_style: &style_structs::Border,
) -> BorderRadius {
// TODO(cgaebel): Support border radii even in the case of multiple border widths.
// This is an extension of supporting elliptical radii. For now, all percentage
// radii will be relative to the width.
handle_overlapping_radii(
abs_bounds.size.to_layout(),
BorderRadius {
top_left: model::specified_border_radius(
border_style.border_top_left_radius,
abs_bounds.size,
).to_layout(),
top_right: model::specified_border_radius(
border_style.border_top_right_radius,
abs_bounds.size,
).to_layout(),
bottom_right: model::specified_border_radius(
border_style.border_bottom_right_radius,
abs_bounds.size,
).to_layout(),
bottom_left: model::specified_border_radius(
border_style.border_bottom_left_radius,
abs_bounds.size,
).to_layout(),
},
)
}
/// Creates a four-sided border with uniform color, width and corner radius.
pub fn simple_normal_border(color: ColorF, style: BorderStyle) -> NormalBorder {
let side = BorderSide { color, style };
NormalBorder {
left: side,
right: side,
top: side,
bottom: side,
radius: BorderRadius::zero(),
do_aa: true,
}
}
/// Calculates radii for the inner side.
///
/// Radii usually describe the outer side of a border but for the lines to look nice
/// the inner radii need to be smaller depending on the line width.
///
/// This is used to determine clipping areas.
pub fn calculate_inner_border_radii(
mut radii: BorderRadius,
offsets: SideOffsets2D<Au>,
) -> BorderRadius {
fn inner_length(x: f32, offset: Au) -> f32 {
0.0_f32.max(x - offset.to_f32_px())
}
radii.top_left.width = inner_length(radii.top_left.width, offsets.left);
radii.bottom_left.width = inner_length(radii.bottom_left.width, offsets.left);
radii.top_right.width = inner_length(radii.top_right.width, offsets.right);
radii.bottom_right.width = inner_length(radii.bottom_right.width, offsets.right);
radii.top_left.height = inner_length(radii.top_left.height, offsets.top);
radii.top_right.height = inner_length(radii.top_right.height, offsets.top);
radii.bottom_left.height = inner_length(radii.bottom_left.height, offsets.bottom);
radii.bottom_right.height = inner_length(radii.bottom_right.height, offsets.bottom);
radii
}
fn calculate_border_image_outset_side(outset: LengthOrNumber, border_width: Au) -> Au {
match outset {
Either::First(length) => length.into(),
Either::Second(factor) => border_width.scale_by(factor),
}
}
pub fn calculate_border_image_outset(
outset: BorderImageOutset,
border: SideOffsets2D<Au>,
) -> SideOffsets2D<Au> {
SideOffsets2D::new(
calculate_border_image_outset_side(outset.0, border.top),
calculate_border_image_outset_side(outset.1, border.right),
calculate_border_image_outset_side(outset.2, border.bottom),
calculate_border_image_outset_side(outset.3, border.left),
)
}
fn calculate_border_image_width_side(
border_image_width: BorderImageSideWidth,
border_width: f32,
total_length: Au,
) -> f32 {
match border_image_width {
GenericBorderImageSideWidth::Length(v) => v.to_used_value(total_length).to_f32_px(),
GenericBorderImageSideWidth::Number(x) => border_width * x,
GenericBorderImageSideWidth::Auto => border_width,
}
}
pub fn calculate_border_image_width(
width: &BorderImageWidth,
border: LayoutSideOffsets,
border_area: Size2D<Au>,
) -> LayoutSideOffsets {
LayoutSideOffsets::new(
calculate_border_image_width_side(width.0, border.top, border_area.height),
calculate_border_image_width_side(width.1, border.right, border_area.width),
calculate_border_image_width_side(width.2, border.bottom, border_area.height),
calculate_border_image_width_side(width.3, border.left, border_area.width),
)
}