layout: Combine layout_2020 and layout_thread_2020 into a crate called layout (#36613)

Now that legacy layout has been removed, the name `layout_2020` doesn't
make much sense any longer, also it's 2025 now for better or worse. The
split between the "layout thread" and "layout" also doesn't make as much
sense since layout doesn't run on it's own thread. There's a possibility
that it will in the future, but that should be something that the user
of the crate controls rather than layout iself.

This is part of the larger layout interface cleanup and optimization
that
@Looriool and I are doing.

Testing: Covered by existing tests as this is just code movement.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-04-19 12:17:03 +02:00 committed by GitHub
parent 3ab5b8c447
commit 7787cab521
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 58 additions and 122 deletions

View file

@ -0,0 +1,361 @@
/* 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::{Point2D, Size2D, Vector2D};
use style::computed_values::background_attachment::SingleComputedValue as BackgroundAttachment;
use style::computed_values::background_clip::single_value::T as Clip;
use style::computed_values::background_origin::single_value::T as Origin;
use style::properties::ComputedValues;
use style::values::computed::LengthPercentage;
use style::values::computed::background::BackgroundSize as Size;
use style::values::specified::background::{
BackgroundRepeat as RepeatXY, BackgroundRepeatKeyword as Repeat,
};
use webrender_api::{self as wr, units};
use wr::ClipChainId;
use wr::units::LayoutSize;
use crate::replaced::NaturalSizes;
pub(super) struct BackgroundLayer {
pub common: wr::CommonItemProperties,
pub bounds: units::LayoutRect,
pub tile_size: units::LayoutSize,
pub tile_spacing: units::LayoutSize,
pub repeat: bool,
}
#[derive(Debug)]
struct Layout1DResult {
repeat: bool,
bounds_origin: f32,
bounds_size: f32,
tile_spacing: f32,
}
fn get_cyclic<T>(values: &[T], layer_index: usize) -> &T {
&values[layer_index % values.len()]
}
pub(super) struct BackgroundPainter<'a> {
pub style: &'a ComputedValues,
pub positioning_area_override: Option<units::LayoutRect>,
pub painting_area_override: Option<units::LayoutRect>,
}
impl<'a> BackgroundPainter<'a> {
/// Get the painting area for this background, which is the actual rectangle in the
/// current coordinate system that the background will be painted.
pub(super) fn painting_area(
&self,
fragment_builder: &'a super::BuilderForBoxFragment,
builder: &mut super::DisplayListBuilder,
layer_index: usize,
) -> units::LayoutRect {
let fb = fragment_builder;
if let Some(painting_area_override) = self.painting_area_override.as_ref() {
return *painting_area_override;
}
if self.positioning_area_override.is_some() {
return fb.border_rect;
}
let background = self.style.get_background();
if &BackgroundAttachment::Fixed ==
get_cyclic(&background.background_attachment.0, layer_index)
{
let viewport_size = builder.display_list.compositor_info.viewport_size;
return units::LayoutRect::from_origin_and_size(Point2D::origin(), viewport_size);
}
match get_cyclic(&background.background_clip.0, layer_index) {
Clip::ContentBox => *fragment_builder.content_rect(),
Clip::PaddingBox => *fragment_builder.padding_rect(),
Clip::BorderBox => fragment_builder.border_rect,
}
}
fn clip(
&self,
fragment_builder: &'a super::BuilderForBoxFragment,
builder: &mut super::DisplayListBuilder,
layer_index: usize,
) -> Option<ClipChainId> {
if self.painting_area_override.is_some() {
return None;
}
if self.positioning_area_override.is_some() {
return fragment_builder.border_edge_clip(builder, false);
}
// The 'backgound-clip' property maps directly to `clip_rect` in `CommonItemProperties`:
let background = self.style.get_background();
let force_clip_creation = get_cyclic(&background.background_attachment.0, layer_index) ==
&BackgroundAttachment::Fixed;
match get_cyclic(&background.background_clip.0, layer_index) {
Clip::ContentBox => fragment_builder.content_edge_clip(builder, force_clip_creation),
Clip::PaddingBox => fragment_builder.padding_edge_clip(builder, force_clip_creation),
Clip::BorderBox => fragment_builder.border_edge_clip(builder, force_clip_creation),
}
}
/// Get the [`wr::CommonItemProperties`] for this background. This includes any clipping
/// established by border radii as well as special clipping and spatial node assignment
/// necessary for `background-attachment`.
pub(super) fn common_properties(
&self,
fragment_builder: &'a super::BuilderForBoxFragment,
builder: &mut super::DisplayListBuilder,
layer_index: usize,
painting_area: units::LayoutRect,
) -> wr::CommonItemProperties {
let clip = self.clip(fragment_builder, builder, layer_index);
let style = &fragment_builder.fragment.style;
let mut common = builder.common_properties(painting_area, style);
if let Some(clip_chain_id) = clip {
common.clip_chain_id = clip_chain_id;
}
if &BackgroundAttachment::Fixed ==
get_cyclic(&style.get_background().background_attachment.0, layer_index)
{
common.spatial_id = builder.current_reference_frame_scroll_node_id.spatial_id;
}
common
}
/// Get the positioning area of the background which is the rectangle that defines where
/// the origin of the background content is, regardless of where the background is actual
/// painted.
pub(super) fn positioning_area(
&self,
fragment_builder: &'a super::BuilderForBoxFragment,
layer_index: usize,
) -> units::LayoutRect {
if let Some(positioning_area_override) = self.positioning_area_override {
return positioning_area_override;
}
match get_cyclic(
&self.style.get_background().background_attachment.0,
layer_index,
) {
BackgroundAttachment::Scroll => match get_cyclic(
&self.style.get_background().background_origin.0,
layer_index,
) {
Origin::ContentBox => *fragment_builder.content_rect(),
Origin::PaddingBox => *fragment_builder.padding_rect(),
Origin::BorderBox => fragment_builder.border_rect,
},
BackgroundAttachment::Fixed => {
// This isn't the viewport size because that rects larger than the viewport might be
// transformed down into areas smaller than the viewport.
units::LayoutRect::from_origin_and_size(
Point2D::origin(),
LayoutSize::new(f32::MAX, f32::MAX),
)
},
}
}
}
pub(super) fn layout_layer(
fragment_builder: &mut super::BuilderForBoxFragment,
painter: &BackgroundPainter,
builder: &mut super::DisplayListBuilder,
layer_index: usize,
natural_sizes: NaturalSizes,
) -> Option<BackgroundLayer> {
let painting_area = painter.painting_area(fragment_builder, builder, layer_index);
let positioning_area = painter.positioning_area(fragment_builder, layer_index);
let common = painter.common_properties(fragment_builder, builder, layer_index, painting_area);
// https://drafts.csswg.org/css-backgrounds/#background-size
enum ContainOrCover {
Contain,
Cover,
}
let size_contain_or_cover = |background_size| {
let mut tile_size = positioning_area.size();
if let Some(natural_ratio) = natural_sizes.ratio {
let positioning_ratio = positioning_area.size().width / positioning_area.size().height;
// Whether the tile width (as opposed to height)
// is scaled to that of the positioning area
let fit_width = match background_size {
ContainOrCover::Contain => positioning_ratio <= natural_ratio,
ContainOrCover::Cover => positioning_ratio > natural_ratio,
};
// The other dimension needs to be adjusted
if fit_width {
tile_size.height = tile_size.width / natural_ratio
} else {
tile_size.width = tile_size.height * natural_ratio
}
}
tile_size
};
let b = painter.style.get_background();
let mut tile_size = match get_cyclic(&b.background_size.0, layer_index) {
Size::Contain => size_contain_or_cover(ContainOrCover::Contain),
Size::Cover => size_contain_or_cover(ContainOrCover::Cover),
Size::ExplicitSize { width, height } => {
let mut width = width.non_auto().map(|lp| {
lp.0.to_used_value(Au::from_f32_px(positioning_area.size().width))
});
let mut height = height.non_auto().map(|lp| {
lp.0.to_used_value(Au::from_f32_px(positioning_area.size().height))
});
if width.is_none() && height.is_none() {
// Both computed values are 'auto':
// use natural sizes, treating missing width or height as 'auto'
width = natural_sizes.width;
height = natural_sizes.height;
}
match (width, height) {
(Some(w), Some(h)) => units::LayoutSize::new(w.to_f32_px(), h.to_f32_px()),
(Some(w), None) => {
let h = if let Some(natural_ratio) = natural_sizes.ratio {
w.scale_by(1.0 / natural_ratio)
} else if let Some(natural_height) = natural_sizes.height {
natural_height
} else {
// Treated as 100%
Au::from_f32_px(positioning_area.size().height)
};
units::LayoutSize::new(w.to_f32_px(), h.to_f32_px())
},
(None, Some(h)) => {
let w = if let Some(natural_ratio) = natural_sizes.ratio {
h.scale_by(natural_ratio)
} else if let Some(natural_width) = natural_sizes.width {
natural_width
} else {
// Treated as 100%
Au::from_f32_px(positioning_area.size().width)
};
units::LayoutSize::new(w.to_f32_px(), h.to_f32_px())
},
// Both comptued values were 'auto', and neither natural size is present
(None, None) => size_contain_or_cover(ContainOrCover::Contain),
}
},
};
if tile_size.width == 0.0 || tile_size.height == 0.0 {
return None;
}
let RepeatXY(repeat_x, repeat_y) = *get_cyclic(&b.background_repeat.0, layer_index);
let result_x = layout_1d(
&mut tile_size.width,
repeat_x,
get_cyclic(&b.background_position_x.0, layer_index),
painting_area.min.x - positioning_area.min.x,
painting_area.size().width,
positioning_area.size().width,
);
let result_y = layout_1d(
&mut tile_size.height,
repeat_y,
get_cyclic(&b.background_position_y.0, layer_index),
painting_area.min.y - positioning_area.min.y,
painting_area.size().height,
positioning_area.size().height,
);
let bounds = units::LayoutRect::from_origin_and_size(
positioning_area.min + Vector2D::new(result_x.bounds_origin, result_y.bounds_origin),
Size2D::new(result_x.bounds_size, result_y.bounds_size),
);
let tile_spacing = units::LayoutSize::new(result_x.tile_spacing, result_y.tile_spacing);
Some(BackgroundLayer {
common,
bounds,
tile_size,
tile_spacing,
repeat: result_x.repeat || result_y.repeat,
})
}
/// Abstract over the horizontal or vertical dimension
/// Coordinates (0, 0) for the purpose of this function are the positioning areas origin.
fn layout_1d(
tile_size: &mut f32,
mut repeat: Repeat,
position: &LengthPercentage,
painting_area_origin: f32,
painting_area_size: f32,
positioning_area_size: f32,
) -> Layout1DResult {
// https://drafts.csswg.org/css-backgrounds/#background-repeat
if let Repeat::Round = repeat {
*tile_size = positioning_area_size / (positioning_area_size / *tile_size).round();
}
// https://drafts.csswg.org/css-backgrounds/#background-position
let mut position = position
.to_used_value(Au::from_f32_px(positioning_area_size - *tile_size))
.to_f32_px();
let mut tile_spacing = 0.0;
// https://drafts.csswg.org/css-backgrounds/#background-repeat
if let Repeat::Space = repeat {
// The most entire tiles we can fit
let tile_count = (positioning_area_size / *tile_size).floor();
if tile_count >= 2.0 {
position = 0.0;
// Make the outsides of the first and last of that many tiles
// touch the edges of the positioning area:
let total_space = positioning_area_size - *tile_size * tile_count;
let spaces_count = tile_count - 1.0;
tile_spacing = total_space / spaces_count;
} else {
repeat = Repeat::NoRepeat
}
}
match repeat {
Repeat::Repeat | Repeat::Round | Repeat::Space => {
// WebRenders `RepeatingImageDisplayItem` contains a `bounds` rectangle and:
//
// * The tiling is clipped to the intersection of `clip_rect` and `bounds`
// * The origin (top-left corner) of `bounds` is the position
// of the “first” (top-left-most) tile.
//
// In the general case that first tile is not the one that is positioned by
// `background-position`.
// We want it to be the top-left-most tile that intersects with `clip_rect`.
// We find it by offsetting by a whole number of strides,
// then compute `bounds` such that:
//
// * Its bottom-right is the bottom-right of `clip_rect`
// * Its top-left is the top-left of first tile.
let tile_stride = *tile_size + tile_spacing;
let offset = position - painting_area_origin;
let bounds_origin = position - tile_stride * (offset / tile_stride).ceil();
let bounds_end = painting_area_origin + painting_area_size;
let bounds_size = bounds_end - bounds_origin;
Layout1DResult {
repeat: true,
bounds_origin,
bounds_size,
tile_spacing,
}
},
Repeat::NoRepeat => {
// `RepeatingImageDisplayItem` always repeats in both dimension.
// When we want only one of the dimensions to repeat,
// we use the `bounds` rectangle to clip the tiling to one tile
// in that dimension.
Layout1DResult {
repeat: false,
bounds_origin: position,
bounds_size: *tile_size,
tile_spacing: 0.0,
}
},
}
}

View file

@ -0,0 +1,259 @@
/* 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 base::id::ScrollTreeNodeId;
use style::values::computed::basic_shape::{BasicShape, ClipPath};
use style::values::computed::length_percentage::NonNegativeLengthPercentage;
use style::values::computed::position::Position;
use style::values::generics::basic_shape::{GenericShapeRadius, ShapeBox, ShapeGeometryBox};
use style::values::generics::position::GenericPositionOrAuto;
use webrender_api::ClipChainId;
use webrender_api::units::{LayoutRect, LayoutSideOffsets, LayoutSize};
use super::{BuilderForBoxFragment, DisplayList, compute_margin_box_radius, normalize_radii};
pub(super) fn build_clip_path_clip_chain_if_necessary(
clip_path: ClipPath,
display_list: &mut DisplayList,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_chain_id: &ClipChainId,
fragment_builder: BuilderForBoxFragment,
) -> Option<ClipChainId> {
let geometry_box = match clip_path {
ClipPath::Shape(_, ShapeGeometryBox::ShapeBox(shape_box)) => shape_box,
ClipPath::Shape(_, ShapeGeometryBox::ElementDependent) => ShapeBox::BorderBox,
ClipPath::Box(ShapeGeometryBox::ShapeBox(shape_box)) => shape_box,
ClipPath::Box(ShapeGeometryBox::ElementDependent) => ShapeBox::BorderBox,
_ => return None,
};
let layout_rect = match geometry_box {
ShapeBox::BorderBox => fragment_builder.border_rect,
ShapeBox::ContentBox => *fragment_builder.content_rect(),
ShapeBox::PaddingBox => *fragment_builder.padding_rect(),
ShapeBox::MarginBox => *fragment_builder.margin_rect(),
};
if let ClipPath::Shape(shape, _) = clip_path {
match *shape {
BasicShape::Circle(_) | BasicShape::Ellipse(_) | BasicShape::Rect(_) => {
build_simple_shape(
*shape,
layout_rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
)
},
BasicShape::Polygon(_) | BasicShape::PathOrShape(_) => None,
}
} else {
Some(create_rect_clip_chain(
match geometry_box {
ShapeBox::MarginBox => compute_margin_box_radius(
fragment_builder.border_radius,
layout_rect.size(),
fragment_builder.fragment,
),
_ => fragment_builder.border_radius,
},
layout_rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
))
}
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
name = "display_list::build_simple_shape",
skip_all,
fields(servo_profiling = true),
level = "trace",
)
)]
fn build_simple_shape(
shape: BasicShape,
layout_box: LayoutRect,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_chain_id: &ClipChainId,
display_list: &mut DisplayList,
) -> Option<ClipChainId> {
match shape {
BasicShape::Rect(rect) => {
let box_height = Au::from_f32_px(layout_box.height());
let box_width = Au::from_f32_px(layout_box.width());
let insets = LayoutSideOffsets::new(
rect.rect.0.to_used_value(box_height).to_f32_px(),
rect.rect.1.to_used_value(box_width).to_f32_px(),
rect.rect.2.to_used_value(box_height).to_f32_px(),
rect.rect.3.to_used_value(box_width).to_f32_px(),
);
// `inner_rect()` will cause an assertion failure if the insets are larger than the
// rectangle dimension.
let shape_rect = if insets.left + insets.right >= layout_box.width() ||
insets.top + insets.bottom > layout_box.height()
{
LayoutRect::from_origin_and_size(layout_box.min, LayoutSize::zero())
} else {
layout_box.to_rect().inner_rect(insets).to_box2d()
};
let corner = |corner: &style::values::computed::BorderCornerRadius| {
LayoutSize::new(
corner.0.width.0.to_used_value(box_width).to_f32_px(),
corner.0.height.0.to_used_value(box_height).to_f32_px(),
)
};
let mut radii = webrender_api::BorderRadius {
top_left: corner(&rect.round.top_left),
top_right: corner(&rect.round.top_right),
bottom_left: corner(&rect.round.bottom_left),
bottom_right: corner(&rect.round.bottom_right),
};
normalize_radii(&layout_box, &mut radii);
Some(create_rect_clip_chain(
radii,
shape_rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
))
},
BasicShape::Circle(circle) => {
let center = match circle.position {
GenericPositionOrAuto::Position(position) => position,
GenericPositionOrAuto::Auto => Position::center(),
};
let anchor_x = center
.horizontal
.to_used_value(Au::from_f32_px(layout_box.width()));
let anchor_y = center
.vertical
.to_used_value(Au::from_f32_px(layout_box.height()));
let center = layout_box
.min
.add_size(&LayoutSize::new(anchor_x.to_f32_px(), anchor_y.to_f32_px()));
let horizontal =
compute_shape_radius(center.x, &circle.radius, layout_box.min.x, layout_box.max.x);
let vertical =
compute_shape_radius(center.y, &circle.radius, layout_box.min.y, layout_box.max.y);
// If the value is `Length` then both values should be the same at this point.
let radius = match circle.radius {
GenericShapeRadius::FarthestSide => horizontal.max(vertical),
GenericShapeRadius::ClosestSide => horizontal.min(vertical),
GenericShapeRadius::Length(_) => horizontal,
};
let radius = LayoutSize::new(radius, radius);
let mut radii = webrender_api::BorderRadius {
top_left: radius,
top_right: radius,
bottom_left: radius,
bottom_right: radius,
};
let start = center.add_size(&-radius);
let rect = LayoutRect::from_origin_and_size(start, radius * 2.);
normalize_radii(&layout_box, &mut radii);
Some(create_rect_clip_chain(
radii,
rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
))
},
BasicShape::Ellipse(ellipse) => {
let center = match ellipse.position {
GenericPositionOrAuto::Position(position) => position,
GenericPositionOrAuto::Auto => Position::center(),
};
let anchor_x = center
.horizontal
.to_used_value(Au::from_f32_px(layout_box.width()));
let anchor_y = center
.vertical
.to_used_value(Au::from_f32_px(layout_box.height()));
let center = layout_box
.min
.add_size(&LayoutSize::new(anchor_x.to_f32_px(), anchor_y.to_f32_px()));
let width = compute_shape_radius(
center.x,
&ellipse.semiaxis_x,
layout_box.min.x,
layout_box.max.x,
);
let height = compute_shape_radius(
center.y,
&ellipse.semiaxis_y,
layout_box.min.y,
layout_box.max.y,
);
let mut radii = webrender_api::BorderRadius {
top_left: LayoutSize::new(width, height),
top_right: LayoutSize::new(width, height),
bottom_left: LayoutSize::new(width, height),
bottom_right: LayoutSize::new(width, height),
};
let size = LayoutSize::new(width, height);
let start = center.add_size(&-size);
let rect = LayoutRect::from_origin_and_size(start, size * 2.);
normalize_radii(&rect, &mut radii);
Some(create_rect_clip_chain(
radii,
rect,
parent_scroll_node_id,
parent_clip_chain_id,
display_list,
))
},
_ => None,
}
}
fn compute_shape_radius(
center: f32,
radius: &GenericShapeRadius<NonNegativeLengthPercentage>,
min_edge: f32,
max_edge: f32,
) -> f32 {
let distance_from_min_edge = (min_edge - center).abs();
let distance_from_max_edge = (max_edge - center).abs();
match radius {
GenericShapeRadius::FarthestSide => distance_from_min_edge.max(distance_from_max_edge),
GenericShapeRadius::ClosestSide => distance_from_min_edge.min(distance_from_max_edge),
GenericShapeRadius::Length(length) => length
.0
.to_used_value(Au::from_f32_px(max_edge - min_edge))
.to_f32_px(),
}
}
fn create_rect_clip_chain(
radii: webrender_api::BorderRadius,
rect: LayoutRect,
parent_scroll_node_id: &ScrollTreeNodeId,
parent_clip_chain_id: &ClipChainId,
display_list: &mut DisplayList,
) -> ClipChainId {
let new_clip_id = if radii.is_zero() {
display_list
.wr
.define_clip_rect(parent_scroll_node_id.spatial_id, rect)
} else {
display_list.wr.define_clip_rounded_rect(
parent_scroll_node_id.spatial_id,
webrender_api::ComplexClipRegion {
rect,
radii,
mode: webrender_api::ClipMode::Clip,
},
)
};
display_list.define_clip_chain(*parent_clip_chain_id, [new_clip_id])
}

View file

@ -0,0 +1,160 @@
/* 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 style::color::AbsoluteColor;
use style::computed_values::image_rendering::T as ComputedImageRendering;
use style::computed_values::mix_blend_mode::T as ComputedMixBlendMode;
use style::computed_values::text_decoration_style::T as ComputedTextDecorationStyle;
use style::computed_values::transform_style::T as ComputedTransformStyle;
use style::values::computed::Filter as ComputedFilter;
use style::values::specified::border::BorderImageRepeatKeyword;
use webrender_api::{
FilterOp, ImageRendering, LineStyle, MixBlendMode, RepeatMode, Shadow, TransformStyle, units,
};
use crate::geom::{PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize};
pub trait ToWebRender {
type Type;
fn to_webrender(&self) -> Self::Type;
}
pub trait FilterToWebRender {
type Type;
fn to_webrender(&self, current_color: &AbsoluteColor) -> Self::Type;
}
impl FilterToWebRender for ComputedFilter {
type Type = FilterOp;
fn to_webrender(&self, current_color: &AbsoluteColor) -> Self::Type {
match *self {
ComputedFilter::Blur(radius) => FilterOp::Blur(radius.px(), radius.px()),
ComputedFilter::Brightness(amount) => FilterOp::Brightness(amount.0),
ComputedFilter::Contrast(amount) => FilterOp::Contrast(amount.0),
ComputedFilter::Grayscale(amount) => FilterOp::Grayscale(amount.0),
ComputedFilter::HueRotate(angle) => FilterOp::HueRotate(angle.radians()),
ComputedFilter::Invert(amount) => FilterOp::Invert(amount.0),
ComputedFilter::Opacity(amount) => FilterOp::Opacity(amount.0.into(), amount.0),
ComputedFilter::Saturate(amount) => FilterOp::Saturate(amount.0),
ComputedFilter::Sepia(amount) => FilterOp::Sepia(amount.0),
ComputedFilter::DropShadow(ref shadow) => FilterOp::DropShadow(Shadow {
blur_radius: shadow.blur.px(),
offset: units::LayoutVector2D::new(shadow.horizontal.px(), shadow.vertical.px()),
color: super::rgba(shadow.color.clone().resolve_to_absolute(current_color)),
}),
// Statically check that Url is impossible.
ComputedFilter::Url(ref url) => match *url {},
}
}
}
impl ToWebRender for ComputedMixBlendMode {
type Type = MixBlendMode;
fn to_webrender(&self) -> Self::Type {
match *self {
ComputedMixBlendMode::Normal => MixBlendMode::Normal,
ComputedMixBlendMode::Multiply => MixBlendMode::Multiply,
ComputedMixBlendMode::Screen => MixBlendMode::Screen,
ComputedMixBlendMode::Overlay => MixBlendMode::Overlay,
ComputedMixBlendMode::Darken => MixBlendMode::Darken,
ComputedMixBlendMode::Lighten => MixBlendMode::Lighten,
ComputedMixBlendMode::ColorDodge => MixBlendMode::ColorDodge,
ComputedMixBlendMode::ColorBurn => MixBlendMode::ColorBurn,
ComputedMixBlendMode::HardLight => MixBlendMode::HardLight,
ComputedMixBlendMode::SoftLight => MixBlendMode::SoftLight,
ComputedMixBlendMode::Difference => MixBlendMode::Difference,
ComputedMixBlendMode::Exclusion => MixBlendMode::Exclusion,
ComputedMixBlendMode::Hue => MixBlendMode::Hue,
ComputedMixBlendMode::Saturation => MixBlendMode::Saturation,
ComputedMixBlendMode::Color => MixBlendMode::Color,
ComputedMixBlendMode::Luminosity => MixBlendMode::Luminosity,
ComputedMixBlendMode::PlusLighter => MixBlendMode::PlusLighter,
}
}
}
impl ToWebRender for ComputedTransformStyle {
type Type = TransformStyle;
fn to_webrender(&self) -> Self::Type {
match *self {
ComputedTransformStyle::Flat => TransformStyle::Flat,
ComputedTransformStyle::Preserve3d => TransformStyle::Preserve3D,
}
}
}
impl ToWebRender for PhysicalPoint<Au> {
type Type = units::LayoutPoint;
fn to_webrender(&self) -> Self::Type {
units::LayoutPoint::new(self.x.to_f32_px(), self.y.to_f32_px())
}
}
impl ToWebRender for PhysicalSize<Au> {
type Type = units::LayoutSize;
fn to_webrender(&self) -> Self::Type {
units::LayoutSize::new(self.width.to_f32_px(), self.height.to_f32_px())
}
}
impl ToWebRender for PhysicalRect<Au> {
type Type = units::LayoutRect;
fn to_webrender(&self) -> Self::Type {
units::LayoutRect::from_origin_and_size(
self.origin.to_webrender(),
self.size.to_webrender(),
)
}
}
impl ToWebRender for PhysicalSides<Au> {
type Type = units::LayoutSideOffsets;
fn to_webrender(&self) -> Self::Type {
units::LayoutSideOffsets::new(
self.top.to_f32_px(),
self.right.to_f32_px(),
self.bottom.to_f32_px(),
self.left.to_f32_px(),
)
}
}
impl ToWebRender for ComputedTextDecorationStyle {
type Type = LineStyle;
fn to_webrender(&self) -> Self::Type {
match *self {
ComputedTextDecorationStyle::Solid => LineStyle::Solid,
ComputedTextDecorationStyle::Dotted => LineStyle::Dotted,
ComputedTextDecorationStyle::Dashed => LineStyle::Dashed,
ComputedTextDecorationStyle::Wavy => LineStyle::Wavy,
_ => LineStyle::Solid,
}
}
}
impl ToWebRender for BorderImageRepeatKeyword {
type Type = RepeatMode;
fn to_webrender(&self) -> Self::Type {
match *self {
BorderImageRepeatKeyword::Stretch => RepeatMode::Stretch,
BorderImageRepeatKeyword::Repeat => RepeatMode::Repeat,
BorderImageRepeatKeyword::Round => RepeatMode::Round,
BorderImageRepeatKeyword::Space => RepeatMode::Space,
}
}
}
impl ToWebRender for ComputedImageRendering {
type Type = ImageRendering;
fn to_webrender(&self) -> Self::Type {
match self {
ComputedImageRendering::Auto => ImageRendering::Auto,
ComputedImageRendering::CrispEdges => ImageRendering::CrispEdges,
ComputedImageRendering::Pixelated => ImageRendering::Pixelated,
}
}
}

View file

@ -0,0 +1,468 @@
/* 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 {
items,
direction,
color_interpolation_method,
flags,
compat_mode: _,
} => build_linear(
style,
items,
direction,
color_interpolation_method,
*flags,
size,
builder,
),
Gradient::Radial {
shape,
position,
color_interpolation_method,
items,
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 lines 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?
// Its debatble whether thats 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?
// Its debatble whether thats 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
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff