mirror of
https://github.com/servo/servo.git
synced 2025-08-05 21:50:18 +01:00
The Servo internal display list items are already pretty much equivalent to the WebRender ones. Except that Servo items contain base information and associated glyphs and gradient stops which are stored implicitly in WebRender. Remove the display items for rectangles, text, images, border, gradients and box shadow and replace them with their WebRender counter parts.
847 lines
31 KiB
Rust
847 lines
31 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 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 and GenericGradientItem.
|
|
|
|
use app_units::Au;
|
|
use display_list::ToLayout;
|
|
use display_list::items::WebRenderImageInfo;
|
|
use euclid::{Point2D, Rect, SideOffsets2D, Size2D, Vector2D};
|
|
use model::{self, 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 as ComputedBackgroundSize};
|
|
use style::values::computed::{LengthOrNumber, LengthOrPercentage, LengthOrPercentageOrAuto};
|
|
use style::values::computed::{NumberOrPercentage, Percentage, Position};
|
|
use style::values::computed::image::{EndingShape, LineDirection};
|
|
use style::values::generics::NonNegative;
|
|
use style::values::generics::background::BackgroundSize;
|
|
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::{BorderDetails, BorderRadius, BorderSide, BorderStyle, ColorF, ExtendMode};
|
|
use webrender_api::{Gradient, GradientStop, LayoutSize, NinePatchBorder, NinePatchBorderSource};
|
|
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,
|
|
}
|
|
|
|
/// Placment information for both image and gradient backgrounds.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct BackgroundPlacement {
|
|
/// Rendering bounds. The background will start in the uppper-left corner
|
|
/// and fill the whole area.
|
|
pub bounds: Rect<Au>,
|
|
/// Background tile size. Some backgrounds are repeated. These are the
|
|
/// dimensions of a single image of the background.
|
|
pub tile_size: Size2D<Au>,
|
|
/// Spacing between tiles. Some backgrounds are not repeated seamless
|
|
/// but have seams between them like tiles in real life.
|
|
pub tile_spacing: Size2D<Au>,
|
|
/// A clip area. While the background is rendered according to all the
|
|
/// measures above it is only shown within these bounds.
|
|
pub clip_rect: Rect<Au>,
|
|
/// Rounded corners for the clip_rect.
|
|
pub clip_radii: BorderRadius,
|
|
/// Whether or not the background is fixed to the viewport.
|
|
pub fixed: bool,
|
|
}
|
|
|
|
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.
|
|
///
|
|
/// This is used for multiple layered background images.
|
|
/// See: https://drafts.csswg.org/css-backgrounds-3/#layering
|
|
pub fn get_cyclic<T>(arr: &[T], index: usize) -> &T {
|
|
&arr[index % arr.len()]
|
|
}
|
|
|
|
/// For a given area and an image compute how big the
|
|
/// image should be displayed on the background.
|
|
fn compute_background_image_size(
|
|
bg_size: ComputedBackgroundSize,
|
|
bounds_size: Size2D<Au>,
|
|
intrinsic_size: Option<Size2D<Au>>,
|
|
) -> Size2D<Au> {
|
|
match intrinsic_size {
|
|
None => match bg_size {
|
|
BackgroundSize::Cover | BackgroundSize::Contain => bounds_size,
|
|
BackgroundSize::Explicit { width, height } => Size2D::new(
|
|
MaybeAuto::from_style(width.0, bounds_size.width)
|
|
.specified_or_default(bounds_size.width),
|
|
MaybeAuto::from_style(height.0, bounds_size.height)
|
|
.specified_or_default(bounds_size.height),
|
|
),
|
|
},
|
|
Some(own_size) => {
|
|
// If `image_aspect_ratio` < `bounds_aspect_ratio`, the image is tall; otherwise, it is
|
|
// wide.
|
|
let image_aspect_ratio = own_size.width.to_f32_px() / own_size.height.to_f32_px();
|
|
let bounds_aspect_ratio =
|
|
bounds_size.width.to_f32_px() / bounds_size.height.to_f32_px();
|
|
match (bg_size, image_aspect_ratio < bounds_aspect_ratio) {
|
|
(BackgroundSize::Contain, false) | (BackgroundSize::Cover, true) => Size2D::new(
|
|
bounds_size.width,
|
|
bounds_size.width.scale_by(image_aspect_ratio.recip()),
|
|
),
|
|
(BackgroundSize::Contain, true) | (BackgroundSize::Cover, false) => Size2D::new(
|
|
bounds_size.height.scale_by(image_aspect_ratio),
|
|
bounds_size.height,
|
|
),
|
|
(
|
|
BackgroundSize::Explicit {
|
|
width,
|
|
height: NonNegative(LengthOrPercentageOrAuto::Auto),
|
|
},
|
|
_,
|
|
) => {
|
|
let width = MaybeAuto::from_style(width.0, bounds_size.width)
|
|
.specified_or_default(own_size.width);
|
|
Size2D::new(width, width.scale_by(image_aspect_ratio.recip()))
|
|
},
|
|
(
|
|
BackgroundSize::Explicit {
|
|
width: NonNegative(LengthOrPercentageOrAuto::Auto),
|
|
height,
|
|
},
|
|
_,
|
|
) => {
|
|
let height = MaybeAuto::from_style(height.0, bounds_size.height)
|
|
.specified_or_default(own_size.height);
|
|
Size2D::new(height.scale_by(image_aspect_ratio), height)
|
|
},
|
|
(BackgroundSize::Explicit { width, height }, _) => Size2D::new(
|
|
MaybeAuto::from_style(width.0, bounds_size.width)
|
|
.specified_or_default(own_size.width),
|
|
MaybeAuto::from_style(height.0, bounds_size.height)
|
|
.specified_or_default(own_size.height),
|
|
),
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn compute_background_clip(
|
|
bg_clip: BackgroundClip,
|
|
absolute_bounds: Rect<Au>,
|
|
border: SideOffsets2D<Au>,
|
|
border_padding: SideOffsets2D<Au>,
|
|
border_radii: BorderRadius,
|
|
) -> (Rect<Au>, BorderRadius) {
|
|
match bg_clip {
|
|
BackgroundClip::BorderBox => (absolute_bounds, border_radii),
|
|
BackgroundClip::PaddingBox => (
|
|
absolute_bounds.inner_rect(border),
|
|
calculate_inner_border_radii(border_radii, border),
|
|
),
|
|
BackgroundClip::ContentBox => (
|
|
absolute_bounds.inner_rect(border_padding),
|
|
calculate_inner_border_radii(border_radii, border_padding),
|
|
),
|
|
}
|
|
}
|
|
|
|
/// Determines where to place an element background image or gradient.
|
|
///
|
|
/// Photos have their resolution as intrinsic size while gradients have
|
|
/// no intrinsic size.
|
|
pub fn compute_background_placement(
|
|
bg: &Background,
|
|
viewport_size: Size2D<Au>,
|
|
absolute_bounds: Rect<Au>,
|
|
intrinsic_size: Option<Size2D<Au>>,
|
|
border: SideOffsets2D<Au>,
|
|
border_padding: SideOffsets2D<Au>,
|
|
border_radii: BorderRadius,
|
|
index: usize,
|
|
) -> BackgroundPlacement {
|
|
let bg_attachment = *get_cyclic(&bg.background_attachment.0, index);
|
|
let bg_clip = *get_cyclic(&bg.background_clip.0, index);
|
|
let bg_origin = *get_cyclic(&bg.background_origin.0, index);
|
|
let bg_position_x = get_cyclic(&bg.background_position_x.0, index);
|
|
let bg_position_y = get_cyclic(&bg.background_position_y.0, index);
|
|
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(
|
|
bg_clip,
|
|
absolute_bounds,
|
|
border,
|
|
border_padding,
|
|
border_radii,
|
|
);
|
|
|
|
let mut fixed = false;
|
|
let mut bounds = match bg_attachment {
|
|
BackgroundAttachment::Scroll => match bg_origin {
|
|
BackgroundOrigin::BorderBox => absolute_bounds,
|
|
BackgroundOrigin::PaddingBox => absolute_bounds.inner_rect(border),
|
|
BackgroundOrigin::ContentBox => absolute_bounds.inner_rect(border_padding),
|
|
},
|
|
BackgroundAttachment::Fixed => {
|
|
fixed = true;
|
|
Rect::new(Point2D::origin(), viewport_size)
|
|
},
|
|
};
|
|
|
|
let mut tile_size = compute_background_image_size(bg_size, bounds.size, intrinsic_size);
|
|
|
|
let mut tile_spacing = Size2D::zero();
|
|
let own_position = bounds.size - tile_size;
|
|
let pos_x = bg_position_x.to_used_value(own_position.width);
|
|
let pos_y = bg_position_y.to_used_value(own_position.height);
|
|
tile_image_axis(
|
|
bg_repeat.0,
|
|
&mut bounds.origin.x,
|
|
&mut bounds.size.width,
|
|
&mut tile_size.width,
|
|
&mut tile_spacing.width,
|
|
pos_x,
|
|
clip_rect.origin.x,
|
|
clip_rect.size.width,
|
|
);
|
|
tile_image_axis(
|
|
bg_repeat.1,
|
|
&mut bounds.origin.y,
|
|
&mut bounds.size.height,
|
|
&mut tile_size.height,
|
|
&mut tile_spacing.height,
|
|
pos_y,
|
|
clip_rect.origin.y,
|
|
clip_rect.size.height,
|
|
);
|
|
|
|
BackgroundPlacement {
|
|
bounds,
|
|
tile_size,
|
|
tile_spacing,
|
|
clip_rect,
|
|
clip_radii,
|
|
fixed,
|
|
}
|
|
}
|
|
|
|
fn tile_image_round(
|
|
position: &mut Au,
|
|
size: &mut Au,
|
|
absolute_anchor_origin: Au,
|
|
image_size: &mut Au,
|
|
) {
|
|
if *size == Au(0) || *image_size == Au(0) {
|
|
*position = Au(0);
|
|
*size = Au(0);
|
|
return;
|
|
}
|
|
|
|
let number_of_tiles = (size.to_f32_px() / image_size.to_f32_px()).round().max(1.0);
|
|
*image_size = *size / (number_of_tiles as i32);
|
|
tile_image(position, size, absolute_anchor_origin, *image_size);
|
|
}
|
|
|
|
fn tile_image_spaced(
|
|
position: &mut Au,
|
|
size: &mut Au,
|
|
tile_spacing: &mut Au,
|
|
absolute_anchor_origin: Au,
|
|
image_size: Au,
|
|
) {
|
|
if *size == Au(0) || image_size == Au(0) {
|
|
*position = Au(0);
|
|
*size = Au(0);
|
|
*tile_spacing = Au(0);
|
|
return;
|
|
}
|
|
|
|
// Per the spec, if the space available is not enough for two images, just tile as
|
|
// normal but only display a single tile.
|
|
if image_size * 2 >= *size {
|
|
tile_image(position, size, absolute_anchor_origin, image_size);
|
|
*tile_spacing = Au(0);
|
|
*size = image_size;
|
|
return;
|
|
}
|
|
|
|
// Take the box size, remove room for two tiles on the edges, and then calculate how many
|
|
// other tiles fit in between them.
|
|
let size_remaining = *size - (image_size * 2);
|
|
let num_middle_tiles = (size_remaining.to_f32_px() / image_size.to_f32_px()).floor() as i32;
|
|
|
|
// Allocate the remaining space as padding between tiles. background-position is ignored
|
|
// as per the spec, so the position is just the box origin. We are also ignoring
|
|
// background-attachment here, which seems unspecced when combined with
|
|
// background-repeat: space.
|
|
let space_for_middle_tiles = image_size * num_middle_tiles;
|
|
*tile_spacing = (size_remaining - space_for_middle_tiles) / (num_middle_tiles + 1);
|
|
}
|
|
|
|
/// Tile an image
|
|
fn tile_image(position: &mut Au, size: &mut Au, absolute_anchor_origin: Au, image_size: Au) {
|
|
// Avoid division by zero below!
|
|
// Images with a zero width or height are not displayed.
|
|
// Therefore the positions do not matter and can be left unchanged.
|
|
// NOTE: A possible optimization is not to build
|
|
// display items in this case at all.
|
|
if image_size == Au(0) {
|
|
return;
|
|
}
|
|
|
|
let delta_pixels = absolute_anchor_origin - *position;
|
|
let image_size_px = image_size.to_f32_px();
|
|
let tile_count = ((delta_pixels.to_f32_px() + image_size_px - 1.0) / image_size_px).floor();
|
|
let offset = image_size * (tile_count as i32);
|
|
let new_position = absolute_anchor_origin - offset;
|
|
*size = *position - new_position + *size;
|
|
*position = new_position;
|
|
}
|
|
|
|
/// For either the x or the y axis ajust various values to account for tiling.
|
|
///
|
|
/// This is done separately for both axes because the repeat keywords may differ.
|
|
fn tile_image_axis(
|
|
repeat: BackgroundRepeatKeyword,
|
|
position: &mut Au,
|
|
size: &mut Au,
|
|
tile_size: &mut Au,
|
|
tile_spacing: &mut Au,
|
|
offset: Au,
|
|
clip_origin: Au,
|
|
clip_size: Au,
|
|
) {
|
|
let absolute_anchor_origin = *position + offset;
|
|
match repeat {
|
|
BackgroundRepeatKeyword::NoRepeat => {
|
|
*position += offset;
|
|
*size = *tile_size;
|
|
},
|
|
BackgroundRepeatKeyword::Repeat => {
|
|
*position = clip_origin;
|
|
*size = clip_size;
|
|
tile_image(position, size, absolute_anchor_origin, *tile_size);
|
|
},
|
|
BackgroundRepeatKeyword::Space => {
|
|
tile_image_spaced(
|
|
position,
|
|
size,
|
|
tile_spacing,
|
|
absolute_anchor_origin,
|
|
*tile_size,
|
|
);
|
|
let combined_tile_size = *tile_size + *tile_spacing;
|
|
*position = clip_origin;
|
|
*size = clip_size;
|
|
tile_image(position, size, absolute_anchor_origin, combined_tile_size);
|
|
},
|
|
BackgroundRepeatKeyword::Round => {
|
|
tile_image_round(position, size, absolute_anchor_origin, tile_size);
|
|
*position = clip_origin;
|
|
*size = clip_size;
|
|
tile_image(position, size, absolute_anchor_origin, *tile_size);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// 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::Deg(270.).radians(),
|
|
X::Right => Angle::Deg(90.).radians(),
|
|
},
|
|
LineDirection::Vertical(y) => match y {
|
|
Y::Top => Angle::Deg(0.).radians(),
|
|
Y::Bottom => Angle::Deg(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, ¢er)
|
|
},
|
|
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, ¢er)
|
|
},
|
|
};
|
|
|
|
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(),
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
/// Given an image and a border style constructs a border image.
|
|
///
|
|
/// See: https://drafts.csswg.org/css-backgrounds-3/#border-images
|
|
pub fn build_image_border_details(
|
|
webrender_image: WebRenderImageInfo,
|
|
border_style_struct: &style_structs::Border,
|
|
outset: SideOffsets2D<f32>,
|
|
) -> Option<BorderDetails> {
|
|
let corners = &border_style_struct.border_image_slice.offsets;
|
|
let border_image_repeat = &border_style_struct.border_image_repeat;
|
|
if let Some(image_key) = webrender_image.key {
|
|
Some(BorderDetails::NinePatch(NinePatchBorder {
|
|
source: NinePatchBorderSource::Image(image_key),
|
|
width: webrender_image.width,
|
|
height: webrender_image.height,
|
|
slice: SideOffsets2D::new(
|
|
corners.0.resolve(webrender_image.height),
|
|
corners.1.resolve(webrender_image.width),
|
|
corners.2.resolve(webrender_image.height),
|
|
corners.3.resolve(webrender_image.width),
|
|
),
|
|
fill: border_style_struct.border_image_slice.fill,
|
|
repeat_horizontal: border_image_repeat.0.to_layout(),
|
|
repeat_vertical: border_image_repeat.1.to_layout(),
|
|
outset: outset,
|
|
}))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
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),
|
|
)
|
|
}
|