aboutsummaryrefslogtreecommitdiffstats
path: root/components/layout/display_list
diff options
context:
space:
mode:
Diffstat (limited to 'components/layout/display_list')
-rw-r--r--components/layout/display_list/background.rs361
-rw-r--r--components/layout/display_list/clip_path.rs259
-rw-r--r--components/layout/display_list/conversions.rs160
-rw-r--r--components/layout/display_list/gradient.rs468
-rw-r--r--components/layout/display_list/mod.rs1511
-rw-r--r--components/layout/display_list/stacking_context.rs1748
6 files changed, 4507 insertions, 0 deletions
diff --git a/components/layout/display_list/background.rs b/components/layout/display_list/background.rs
new file mode 100644
index 00000000000..f49ddfbe6ce
--- /dev/null
+++ b/components/layout/display_list/background.rs
@@ -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 area’s 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 => {
+ // WebRender’s `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,
+ }
+ },
+ }
+}
diff --git a/components/layout/display_list/clip_path.rs b/components/layout/display_list/clip_path.rs
new file mode 100644
index 00000000000..419d15c3572
--- /dev/null
+++ b/components/layout/display_list/clip_path.rs
@@ -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])
+}
diff --git a/components/layout/display_list/conversions.rs b/components/layout/display_list/conversions.rs
new file mode 100644
index 00000000000..6d78a17e886
--- /dev/null
+++ b/components/layout/display_list/conversions.rs
@@ -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,
+ }
+ }
+}
diff --git a/components/layout/display_list/gradient.rs b/components/layout/display_list/gradient.rs
new file mode 100644
index 00000000000..32d011b4454
--- /dev/null
+++ b/components/layout/display_list/gradient.rs
@@ -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 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
+}
diff --git a/components/layout/display_list/mod.rs b/components/layout/display_list/mod.rs
new file mode 100644
index 00000000000..fa313b306f4
--- /dev/null
+++ b/components/layout/display_list/mod.rs
@@ -0,0 +1,1511 @@
+/* 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 std::cell::{OnceCell, RefCell};
+use std::sync::Arc;
+
+use app_units::{AU_PER_PX, Au};
+use base::WebRenderEpochToU16;
+use base::id::ScrollTreeNodeId;
+use compositing_traits::display_list::{AxesScrollSensitivity, CompositorDisplayListInfo};
+use embedder_traits::Cursor;
+use euclid::{Point2D, SideOffsets2D, Size2D, UnknownUnit};
+use fonts::GlyphStore;
+use gradient::WebRenderGradient;
+use range::Range as ServoRange;
+use servo_geometry::MaxRect;
+use style::Zero;
+use style::color::{AbsoluteColor, ColorSpace};
+use style::computed_values::border_image_outset::T as BorderImageOutset;
+use style::computed_values::text_decoration_style::T as ComputedTextDecorationStyle;
+use style::dom::OpaqueNode;
+use style::properties::ComputedValues;
+use style::properties::longhands::visibility::computed_value::T as Visibility;
+use style::properties::style_structs::Border;
+use style::values::computed::{
+ BorderImageSideWidth, BorderImageWidth, BorderStyle, LengthPercentage,
+ NonNegativeLengthOrNumber, NumberOrPercentage, OutlineStyle,
+};
+use style::values::generics::NonNegative;
+use style::values::generics::rect::Rect;
+use style::values::specified::text::TextDecorationLine;
+use style::values::specified::ui::CursorKind;
+use webrender_api::units::{DevicePixel, LayoutPixel, LayoutRect, LayoutSize};
+use webrender_api::{
+ self as wr, BorderDetails, BoxShadowClipMode, ClipChainId, CommonItemProperties,
+ ImageRendering, NinePatchBorder, NinePatchBorderSource, units,
+};
+use wr::units::LayoutVector2D;
+
+use crate::context::{LayoutContext, ResolvedImage};
+use crate::display_list::conversions::ToWebRender;
+use crate::display_list::stacking_context::StackingContextSection;
+use crate::fragment_tree::{
+ BackgroundMode, BoxFragment, Fragment, FragmentFlags, FragmentTree, SpecificLayoutInfo, Tag,
+ TextFragment,
+};
+use crate::geom::{
+ LengthPercentageOrAuto, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize,
+};
+use crate::replaced::NaturalSizes;
+use crate::style_ext::{BorderStyleColor, ComputedValuesExt};
+
+mod background;
+mod clip_path;
+mod conversions;
+mod gradient;
+mod stacking_context;
+
+use background::BackgroundPainter;
+pub use stacking_context::*;
+
+#[derive(Clone, Copy)]
+pub struct WebRenderImageInfo {
+ pub size: Size2D<u32, UnknownUnit>,
+ pub key: Option<wr::ImageKey>,
+}
+
+// webrender's `ItemTag` is private.
+type ItemTag = (u64, u16);
+type HitInfo = Option<ItemTag>;
+const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(AU_PER_PX);
+
+/// Where the information that's used to build display lists is stored. This
+/// includes both a [wr::DisplayListBuilder] for building up WebRender-specific
+/// display list information and a [CompositorDisplayListInfo] used to store
+/// information used by the compositor, such as a compositor-side scroll tree.
+pub struct DisplayList {
+ /// The [wr::DisplayListBuilder] used to collect display list items.
+ pub wr: wr::DisplayListBuilder,
+
+ /// The information about the WebRender display list that the compositor
+ /// consumes. This curerntly contains the out-of-band hit testing information
+ /// data structure that the compositor uses to map hit tests to information
+ /// about the item hit.
+ pub compositor_info: CompositorDisplayListInfo,
+
+ /// A count of the number of SpatialTree nodes pushed to the WebRender display
+ /// list. This is merely to ensure that the currently-unused SpatialTreeItemKey
+ /// produced for every SpatialTree node is unique.
+ pub spatial_tree_count: u64,
+}
+
+impl DisplayList {
+ /// Create a new [DisplayList] given the dimensions of the layout and the WebRender
+ /// pipeline id.
+ pub fn new(
+ viewport_size: units::LayoutSize,
+ content_size: units::LayoutSize,
+ pipeline_id: wr::PipelineId,
+ epoch: wr::Epoch,
+ viewport_scroll_sensitivity: AxesScrollSensitivity,
+ first_reflow: bool,
+ ) -> Self {
+ Self {
+ wr: wr::DisplayListBuilder::new(pipeline_id),
+ compositor_info: CompositorDisplayListInfo::new(
+ viewport_size,
+ content_size,
+ pipeline_id,
+ epoch,
+ viewport_scroll_sensitivity,
+ first_reflow,
+ ),
+ spatial_tree_count: 0,
+ }
+ }
+
+ pub fn define_clip_chain<I>(&mut self, parent: ClipChainId, clips: I) -> ClipChainId
+ where
+ I: IntoIterator<Item = wr::ClipId>,
+ I::IntoIter: ExactSizeIterator + Clone,
+ {
+ // WebRender has two different ways of expressing "no clip." ClipChainId::INVALID should be
+ // used for primitives, but `None` is used for stacking contexts and clip chains. We convert
+ // to the `Option<ClipChainId>` representation here. Just passing Some(ClipChainId::INVALID)
+ // leads to a crash.
+ let parent = match parent {
+ ClipChainId::INVALID => None,
+ parent => Some(parent),
+ };
+ self.wr.define_clip_chain(parent, clips)
+ }
+}
+
+pub(crate) struct DisplayListBuilder<'a> {
+ /// The current [ScrollTreeNodeId] for this [DisplayListBuilder]. This
+ /// allows only passing the builder instead passing the containing
+ /// [stacking_context::StackingContextContent::Fragment] as an argument to display
+ /// list building functions.
+ current_scroll_node_id: ScrollTreeNodeId,
+
+ /// The current [ScrollTreeNodeId] for this [DisplayListBuilder]. This is necessary in addition
+ /// to the [Self::current_scroll_node_id], because some pieces of fragments as backgrounds with
+ /// `background-attachment: fixed` need to not scroll while the rest of the fragment does.
+ current_reference_frame_scroll_node_id: ScrollTreeNodeId,
+
+ /// The current [wr::ClipId] for this [DisplayListBuilder]. This allows
+ /// only passing the builder instead passing the containing
+ /// [stacking_context::StackingContextContent::Fragment] as an argument to display
+ /// list building functions.
+ current_clip_chain_id: ClipChainId,
+
+ /// The [OpaqueNode] handle to the node used to paint the page background
+ /// if the background was a canvas.
+ element_for_canvas_background: OpaqueNode,
+
+ /// A [LayoutContext] used to get information about the device pixel ratio
+ /// and get handles to WebRender images.
+ pub context: &'a LayoutContext<'a>,
+
+ /// The [DisplayList] used to collect display list items and metadata.
+ pub display_list: &'a mut DisplayList,
+}
+
+impl DisplayList {
+ /// Build the display list, returning true if it was contentful.
+ pub fn build(
+ &mut self,
+ context: &LayoutContext,
+ fragment_tree: &FragmentTree,
+ root_stacking_context: &StackingContext,
+ ) {
+ #[cfg(feature = "tracing")]
+ let _span = tracing::trace_span!("display_list::build", servo_profiling = true).entered();
+ let mut builder = DisplayListBuilder {
+ current_scroll_node_id: self.compositor_info.root_reference_frame_id,
+ current_reference_frame_scroll_node_id: self.compositor_info.root_reference_frame_id,
+ current_clip_chain_id: ClipChainId::INVALID,
+ element_for_canvas_background: fragment_tree.canvas_background.from_element,
+ context,
+ display_list: self,
+ };
+ fragment_tree.build_display_list(&mut builder, root_stacking_context);
+ }
+}
+
+impl DisplayListBuilder<'_> {
+ fn wr(&mut self) -> &mut wr::DisplayListBuilder {
+ &mut self.display_list.wr
+ }
+
+ fn mark_is_contentful(&mut self) {
+ self.display_list.compositor_info.is_contentful = true;
+ }
+
+ fn common_properties(
+ &self,
+ clip_rect: units::LayoutRect,
+ style: &ComputedValues,
+ ) -> wr::CommonItemProperties {
+ // TODO(mrobinson): We should take advantage of this field to pass hit testing
+ // information. This will allow us to avoid creating hit testing display items
+ // for fragments that paint their entire border rectangle.
+ wr::CommonItemProperties {
+ clip_rect,
+ spatial_id: self.current_scroll_node_id.spatial_id,
+ clip_chain_id: self.current_clip_chain_id,
+ flags: style.get_webrender_primitive_flags(),
+ }
+ }
+
+ fn hit_info(
+ &mut self,
+ style: &ComputedValues,
+ tag: Option<Tag>,
+ auto_cursor: Cursor,
+ ) -> HitInfo {
+ use style::computed_values::pointer_events::T as PointerEvents;
+
+ let inherited_ui = style.get_inherited_ui();
+ if inherited_ui.pointer_events == PointerEvents::None {
+ return None;
+ }
+
+ let hit_test_index = self.display_list.compositor_info.add_hit_test_info(
+ tag?.node.0 as u64,
+ Some(cursor(inherited_ui.cursor.keyword, auto_cursor)),
+ self.current_scroll_node_id,
+ );
+ Some((
+ hit_test_index as u64,
+ self.display_list.compositor_info.epoch.as_u16(),
+ ))
+ }
+}
+
+impl Fragment {
+ pub(crate) fn build_display_list(
+ &self,
+ builder: &mut DisplayListBuilder,
+ containing_block: &PhysicalRect<Au>,
+ section: StackingContextSection,
+ is_hit_test_for_scrollable_overflow: bool,
+ is_collapsed_table_borders: bool,
+ ) {
+ match self {
+ Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => {
+ let box_fragment = &*box_fragment.borrow();
+ match box_fragment.style.get_inherited_box().visibility {
+ Visibility::Visible => BuilderForBoxFragment::new(
+ box_fragment,
+ containing_block,
+ is_hit_test_for_scrollable_overflow,
+ is_collapsed_table_borders,
+ )
+ .build(builder, section),
+ Visibility::Hidden => (),
+ Visibility::Collapse => (),
+ }
+ },
+ Fragment::AbsoluteOrFixedPositioned(_) => {},
+ Fragment::Positioning(positioning_fragment) => {
+ let positioning_fragment = positioning_fragment.borrow();
+ if let Some(style) = positioning_fragment.style.as_ref() {
+ let rect = positioning_fragment
+ .rect
+ .translate(containing_block.origin.to_vector());
+ self.maybe_push_hit_test_for_style_and_tag(
+ builder,
+ style,
+ positioning_fragment.base.tag,
+ rect,
+ Cursor::Default,
+ );
+ }
+ },
+ Fragment::Image(image) => {
+ let image = image.borrow();
+ match image.style.get_inherited_box().visibility {
+ Visibility::Visible => {
+ builder.mark_is_contentful();
+
+ let image_rendering = image
+ .style
+ .get_inherited_box()
+ .image_rendering
+ .to_webrender();
+ let rect = image
+ .rect
+ .translate(containing_block.origin.to_vector())
+ .to_webrender();
+ let clip = image
+ .clip
+ .translate(containing_block.origin.to_vector())
+ .to_webrender();
+ let common = builder.common_properties(clip, &image.style);
+
+ if let Some(image_key) = image.image_key {
+ builder.wr().push_image(
+ &common,
+ rect,
+ image_rendering,
+ wr::AlphaType::PremultipliedAlpha,
+ image_key,
+ wr::ColorF::WHITE,
+ );
+ }
+ },
+ Visibility::Hidden => (),
+ Visibility::Collapse => (),
+ }
+ },
+ Fragment::IFrame(iframe) => {
+ let iframe = iframe.borrow();
+ match iframe.style.get_inherited_box().visibility {
+ Visibility::Visible => {
+ builder.mark_is_contentful();
+ let rect = iframe.rect.translate(containing_block.origin.to_vector());
+
+ let common = builder.common_properties(rect.to_webrender(), &iframe.style);
+ builder.wr().push_iframe(
+ rect.to_webrender(),
+ common.clip_rect,
+ &wr::SpaceAndClipInfo {
+ spatial_id: common.spatial_id,
+ clip_chain_id: common.clip_chain_id,
+ },
+ iframe.pipeline_id.into(),
+ true,
+ );
+ },
+ Visibility::Hidden => (),
+ Visibility::Collapse => (),
+ }
+ },
+ Fragment::Text(text) => {
+ let text = &*text.borrow();
+ match text.parent_style.get_inherited_box().visibility {
+ Visibility::Visible => {
+ self.build_display_list_for_text_fragment(text, builder, containing_block)
+ },
+ Visibility::Hidden => (),
+ Visibility::Collapse => (),
+ }
+ },
+ }
+ }
+
+ fn maybe_push_hit_test_for_style_and_tag(
+ &self,
+ builder: &mut DisplayListBuilder,
+ style: &ComputedValues,
+ tag: Option<Tag>,
+ rect: PhysicalRect<Au>,
+ cursor: Cursor,
+ ) {
+ let hit_info = builder.hit_info(style, tag, cursor);
+ let hit_info = match hit_info {
+ Some(hit_info) => hit_info,
+ None => return,
+ };
+
+ let clip_chain_id = builder.current_clip_chain_id;
+ let spatial_id = builder.current_scroll_node_id.spatial_id;
+ builder.wr().push_hit_test(
+ rect.to_webrender(),
+ clip_chain_id,
+ spatial_id,
+ style.get_webrender_primitive_flags(),
+ hit_info,
+ );
+ }
+
+ fn build_display_list_for_text_fragment(
+ &self,
+ fragment: &TextFragment,
+ builder: &mut DisplayListBuilder,
+ containing_block: &PhysicalRect<Au>,
+ ) {
+ // NB: The order of painting text components (CSS Text Decoration Module Level 3) is:
+ // shadows, underline, overline, text, text-emphasis, and then line-through.
+
+ builder.mark_is_contentful();
+
+ let rect = fragment.rect.translate(containing_block.origin.to_vector());
+ let mut baseline_origin = rect.origin;
+ baseline_origin.y += fragment.font_metrics.ascent;
+ let glyphs = glyphs(
+ &fragment.glyphs,
+ baseline_origin,
+ fragment.justification_adjustment,
+ !fragment.has_selection(),
+ );
+ if glyphs.is_empty() {
+ return;
+ }
+
+ self.maybe_push_hit_test_for_style_and_tag(
+ builder,
+ &fragment.parent_style,
+ fragment.base.tag,
+ rect,
+ Cursor::Text,
+ );
+
+ let color = fragment.parent_style.clone_color();
+ let font_metrics = &fragment.font_metrics;
+ let dppx = builder.context.style_context.device_pixel_ratio().get();
+ let common = builder.common_properties(rect.to_webrender(), &fragment.parent_style);
+
+ // Shadows. According to CSS-BACKGROUNDS, text shadows render in *reverse* order (front to
+ // back).
+ let shadows = &fragment.parent_style.get_inherited_text().text_shadow;
+ for shadow in shadows.0.iter().rev() {
+ builder.wr().push_shadow(
+ &wr::SpaceAndClipInfo {
+ spatial_id: common.spatial_id,
+ clip_chain_id: common.clip_chain_id,
+ },
+ wr::Shadow {
+ offset: LayoutVector2D::new(shadow.horizontal.px(), shadow.vertical.px()),
+ color: rgba(shadow.color.resolve_to_absolute(&color)),
+ blur_radius: shadow.blur.px(),
+ },
+ true, /* should_inflate */
+ );
+ }
+
+ if fragment
+ .text_decoration_line
+ .contains(TextDecorationLine::UNDERLINE)
+ {
+ let mut rect = rect;
+ rect.origin.y += font_metrics.ascent - font_metrics.underline_offset;
+ rect.size.height = Au::from_f32_px(font_metrics.underline_size.to_nearest_pixel(dppx));
+ self.build_display_list_for_text_decoration(fragment, builder, &rect, &color);
+ }
+
+ if fragment
+ .text_decoration_line
+ .contains(TextDecorationLine::OVERLINE)
+ {
+ let mut rect = rect;
+ rect.size.height = Au::from_f32_px(font_metrics.underline_size.to_nearest_pixel(dppx));
+ self.build_display_list_for_text_decoration(fragment, builder, &rect, &color);
+ }
+
+ // TODO: This caret/text selection implementation currently does not account for vertical text
+ // and RTL text properly.
+ if let Some(range) = fragment.selection_range {
+ let baseline_origin = rect.origin;
+ if !range.is_empty() {
+ let start = glyphs_advance_by_index(
+ &fragment.glyphs,
+ range.begin(),
+ baseline_origin,
+ fragment.justification_adjustment,
+ );
+
+ let end = glyphs_advance_by_index(
+ &fragment.glyphs,
+ range.end(),
+ baseline_origin,
+ fragment.justification_adjustment,
+ );
+
+ let selection_rect = LayoutRect::new(
+ Point2D::new(start.x.to_f32_px(), containing_block.min_y().to_f32_px()),
+ Point2D::new(end.x.to_f32_px(), containing_block.max_y().to_f32_px()),
+ );
+ if let Some(selection_color) = fragment
+ .selected_style
+ .clone_background_color()
+ .as_absolute()
+ {
+ let selection_common =
+ builder.common_properties(selection_rect, &fragment.parent_style);
+ builder.wr().push_rect(
+ &selection_common,
+ selection_rect,
+ rgba(*selection_color),
+ );
+ }
+ } else {
+ let insertion_point = glyphs_advance_by_index(
+ &fragment.glyphs,
+ range.begin(),
+ baseline_origin,
+ fragment.justification_adjustment,
+ );
+
+ let insertion_point_rect = LayoutRect::new(
+ Point2D::new(
+ insertion_point.x.to_f32_px(),
+ containing_block.min_y().to_f32_px(),
+ ),
+ Point2D::new(
+ insertion_point.x.to_f32_px() + INSERTION_POINT_LOGICAL_WIDTH.to_f32_px(),
+ containing_block.max_y().to_f32_px(),
+ ),
+ );
+ let insertion_point_common =
+ builder.common_properties(insertion_point_rect, &fragment.parent_style);
+ // TODO: The color of the caret is currently hardcoded to the text color.
+ // We should be retrieving the caret color from the style properly.
+ builder
+ .wr()
+ .push_rect(&insertion_point_common, insertion_point_rect, rgba(color));
+ }
+ }
+
+ builder.wr().push_text(
+ &common,
+ rect.to_webrender(),
+ &glyphs,
+ fragment.font_key,
+ rgba(color),
+ None,
+ );
+
+ if fragment
+ .text_decoration_line
+ .contains(TextDecorationLine::LINE_THROUGH)
+ {
+ let mut rect = rect;
+ rect.origin.y += font_metrics.ascent - font_metrics.strikeout_offset;
+ rect.size.height = Au::from_f32_px(font_metrics.strikeout_size.to_nearest_pixel(dppx));
+ self.build_display_list_for_text_decoration(fragment, builder, &rect, &color);
+ }
+
+ if !shadows.0.is_empty() {
+ builder.wr().pop_all_shadows();
+ }
+ }
+
+ fn build_display_list_for_text_decoration(
+ &self,
+ fragment: &TextFragment,
+ builder: &mut DisplayListBuilder,
+ rect: &PhysicalRect<Au>,
+ color: &AbsoluteColor,
+ ) {
+ let rect = rect.to_webrender();
+ let wavy_line_thickness = (0.33 * rect.size().height).ceil();
+ let text_decoration_color = fragment
+ .parent_style
+ .clone_text_decoration_color()
+ .resolve_to_absolute(color);
+ let text_decoration_style = fragment.parent_style.clone_text_decoration_style();
+ if text_decoration_style == ComputedTextDecorationStyle::MozNone {
+ return;
+ }
+ builder.display_list.wr.push_line(
+ &builder.common_properties(rect, &fragment.parent_style),
+ &rect,
+ wavy_line_thickness,
+ wr::LineOrientation::Horizontal,
+ &rgba(text_decoration_color),
+ text_decoration_style.to_webrender(),
+ );
+ // XXX(ferjm) support text-decoration-style: double
+ }
+}
+
+struct BuilderForBoxFragment<'a> {
+ fragment: &'a BoxFragment,
+ containing_block: &'a PhysicalRect<Au>,
+ border_rect: units::LayoutRect,
+ margin_rect: OnceCell<units::LayoutRect>,
+ padding_rect: OnceCell<units::LayoutRect>,
+ content_rect: OnceCell<units::LayoutRect>,
+ border_radius: wr::BorderRadius,
+ border_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
+ padding_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
+ content_edge_clip_chain_id: RefCell<Option<ClipChainId>>,
+ is_hit_test_for_scrollable_overflow: bool,
+ is_collapsed_table_borders: bool,
+}
+
+impl<'a> BuilderForBoxFragment<'a> {
+ fn new(
+ fragment: &'a BoxFragment,
+ containing_block: &'a PhysicalRect<Au>,
+ is_hit_test_for_scrollable_overflow: bool,
+ is_collapsed_table_borders: bool,
+ ) -> Self {
+ let border_rect = fragment
+ .border_rect()
+ .translate(containing_block.origin.to_vector());
+
+ let webrender_border_rect = border_rect.to_webrender();
+ let border_radius = {
+ let resolve = |radius: &LengthPercentage, box_size: Au| {
+ radius.to_used_value(box_size).to_f32_px()
+ };
+ let corner = |corner: &style::values::computed::BorderCornerRadius| {
+ Size2D::new(
+ resolve(&corner.0.width.0, border_rect.size.width),
+ resolve(&corner.0.height.0, border_rect.size.height),
+ )
+ };
+ let b = fragment.style.get_border();
+ let mut radius = wr::BorderRadius {
+ top_left: corner(&b.border_top_left_radius),
+ top_right: corner(&b.border_top_right_radius),
+ bottom_right: corner(&b.border_bottom_right_radius),
+ bottom_left: corner(&b.border_bottom_left_radius),
+ };
+
+ normalize_radii(&webrender_border_rect, &mut radius);
+ radius
+ };
+
+ Self {
+ fragment,
+ containing_block,
+ border_rect: webrender_border_rect,
+ border_radius,
+ margin_rect: OnceCell::new(),
+ padding_rect: OnceCell::new(),
+ content_rect: OnceCell::new(),
+ border_edge_clip_chain_id: RefCell::new(None),
+ padding_edge_clip_chain_id: RefCell::new(None),
+ content_edge_clip_chain_id: RefCell::new(None),
+ is_hit_test_for_scrollable_overflow,
+ is_collapsed_table_borders,
+ }
+ }
+
+ fn content_rect(&self) -> &units::LayoutRect {
+ self.content_rect.get_or_init(|| {
+ self.fragment
+ .content_rect
+ .translate(self.containing_block.origin.to_vector())
+ .to_webrender()
+ })
+ }
+
+ fn padding_rect(&self) -> &units::LayoutRect {
+ self.padding_rect.get_or_init(|| {
+ self.fragment
+ .padding_rect()
+ .translate(self.containing_block.origin.to_vector())
+ .to_webrender()
+ })
+ }
+
+ fn margin_rect(&self) -> &units::LayoutRect {
+ self.margin_rect.get_or_init(|| {
+ self.fragment
+ .margin_rect()
+ .translate(self.containing_block.origin.to_vector())
+ .to_webrender()
+ })
+ }
+
+ fn border_edge_clip(
+ &self,
+ builder: &mut DisplayListBuilder,
+ force_clip_creation: bool,
+ ) -> Option<ClipChainId> {
+ if let Some(clip) = *self.border_edge_clip_chain_id.borrow() {
+ return Some(clip);
+ }
+
+ let maybe_clip = create_clip_chain(
+ self.border_radius,
+ self.border_rect,
+ builder,
+ force_clip_creation,
+ );
+ *self.border_edge_clip_chain_id.borrow_mut() = maybe_clip;
+ maybe_clip
+ }
+
+ fn padding_edge_clip(
+ &self,
+ builder: &mut DisplayListBuilder,
+ force_clip_creation: bool,
+ ) -> Option<ClipChainId> {
+ if let Some(clip) = *self.padding_edge_clip_chain_id.borrow() {
+ return Some(clip);
+ }
+
+ let radii = inner_radii(self.border_radius, self.fragment.border.to_webrender());
+ let maybe_clip =
+ create_clip_chain(radii, *self.padding_rect(), builder, force_clip_creation);
+ *self.padding_edge_clip_chain_id.borrow_mut() = maybe_clip;
+ maybe_clip
+ }
+
+ fn content_edge_clip(
+ &self,
+ builder: &mut DisplayListBuilder,
+ force_clip_creation: bool,
+ ) -> Option<ClipChainId> {
+ if let Some(clip) = *self.content_edge_clip_chain_id.borrow() {
+ return Some(clip);
+ }
+
+ let radii = inner_radii(
+ self.border_radius,
+ (self.fragment.border + self.fragment.padding).to_webrender(),
+ );
+ let maybe_clip =
+ create_clip_chain(radii, *self.content_rect(), builder, force_clip_creation);
+ *self.content_edge_clip_chain_id.borrow_mut() = maybe_clip;
+ maybe_clip
+ }
+
+ fn build(&mut self, builder: &mut DisplayListBuilder, section: StackingContextSection) {
+ if self.is_hit_test_for_scrollable_overflow {
+ self.build_hit_test(builder, self.fragment.scrollable_overflow().to_webrender());
+ return;
+ }
+
+ if self.is_collapsed_table_borders {
+ self.build_collapsed_table_borders(builder);
+ return;
+ }
+
+ if section == StackingContextSection::Outline {
+ self.build_outline(builder);
+ return;
+ }
+
+ self.build_hit_test(builder, self.border_rect);
+ if self
+ .fragment
+ .base
+ .flags
+ .contains(FragmentFlags::DO_NOT_PAINT)
+ {
+ return;
+ }
+ self.build_background(builder);
+ self.build_box_shadow(builder);
+ self.build_border(builder);
+ }
+
+ fn build_hit_test(&self, builder: &mut DisplayListBuilder, rect: LayoutRect) {
+ let hit_info = builder.hit_info(
+ &self.fragment.style,
+ self.fragment.base.tag,
+ Cursor::Default,
+ );
+ let hit_info = match hit_info {
+ Some(hit_info) => hit_info,
+ None => return,
+ };
+
+ let mut common = builder.common_properties(rect, &self.fragment.style);
+ if let Some(clip_chain_id) = self.border_edge_clip(builder, false) {
+ common.clip_chain_id = clip_chain_id;
+ }
+ builder.wr().push_hit_test(
+ common.clip_rect,
+ common.clip_chain_id,
+ common.spatial_id,
+ common.flags,
+ hit_info,
+ );
+ }
+
+ fn build_background_for_painter(
+ &mut self,
+ builder: &mut DisplayListBuilder,
+ painter: &BackgroundPainter,
+ ) {
+ let b = painter.style.get_background();
+ let background_color = painter.style.resolve_color(&b.background_color);
+ if background_color.alpha > 0.0 {
+ // https://drafts.csswg.org/css-backgrounds/#background-color
+ // “The background color is clipped according to the background-clip
+ // value associated with the bottom-most background image layer.”
+ let layer_index = b.background_image.0.len() - 1;
+ let bounds = painter.painting_area(self, builder, layer_index);
+ let common = painter.common_properties(self, builder, layer_index, bounds);
+ builder
+ .wr()
+ .push_rect(&common, bounds, rgba(background_color))
+ }
+
+ self.build_background_image(builder, painter);
+ }
+
+ fn build_background(&mut self, builder: &mut DisplayListBuilder) {
+ if self
+ .fragment
+ .base
+ .is_for_node(builder.element_for_canvas_background)
+ {
+ // This background is already painted for the canvas, don’t paint it again here.
+ return;
+ }
+
+ // If this BoxFragment does not paint a background, do nothing.
+ if let BackgroundMode::None = self.fragment.background_mode {
+ return;
+ }
+
+ // Paint all extra backgrounds for this BoxFragment. These are painted first, as that's
+ // the order that they are expected to be painted for table cells (where this feature
+ // is used).
+ if let BackgroundMode::Extra(ref extra_backgrounds) = self.fragment.background_mode {
+ for extra_background in extra_backgrounds {
+ let positioning_area = extra_background.rect;
+ let painter = BackgroundPainter {
+ style: &extra_background.style,
+ painting_area_override: None,
+ positioning_area_override: Some(
+ positioning_area
+ .translate(self.containing_block.origin.to_vector())
+ .to_webrender(),
+ ),
+ };
+ self.build_background_for_painter(builder, &painter);
+ }
+ }
+
+ let painter = BackgroundPainter {
+ style: &self.fragment.style,
+ painting_area_override: None,
+ positioning_area_override: None,
+ };
+ self.build_background_for_painter(builder, &painter);
+ }
+
+ fn build_background_image(
+ &mut self,
+ builder: &mut DisplayListBuilder,
+ painter: &BackgroundPainter,
+ ) {
+ let style = painter.style;
+ let b = style.get_background();
+ let node = self.fragment.base.tag.map(|tag| tag.node);
+ // Reverse because the property is top layer first, we want to paint bottom layer first.
+ for (index, image) in b.background_image.0.iter().enumerate().rev() {
+ match builder.context.resolve_image(node, image) {
+ None => {},
+ Some(ResolvedImage::Gradient(gradient)) => {
+ let intrinsic = NaturalSizes::empty();
+ let Some(layer) =
+ &background::layout_layer(self, painter, builder, index, intrinsic)
+ else {
+ continue;
+ };
+
+ match gradient::build(style, gradient, layer.tile_size, builder) {
+ WebRenderGradient::Linear(linear_gradient) => builder.wr().push_gradient(
+ &layer.common,
+ layer.bounds,
+ linear_gradient,
+ layer.tile_size,
+ layer.tile_spacing,
+ ),
+ WebRenderGradient::Radial(radial_gradient) => {
+ builder.wr().push_radial_gradient(
+ &layer.common,
+ layer.bounds,
+ radial_gradient,
+ layer.tile_size,
+ layer.tile_spacing,
+ )
+ },
+ WebRenderGradient::Conic(conic_gradient) => {
+ builder.wr().push_conic_gradient(
+ &layer.common,
+ layer.bounds,
+ conic_gradient,
+ layer.tile_size,
+ layer.tile_spacing,
+ )
+ },
+ }
+ },
+ Some(ResolvedImage::Image(image_info)) => {
+ // FIXME: https://drafts.csswg.org/css-images-4/#the-image-resolution
+ let dppx = 1.0;
+ let intrinsic = NaturalSizes::from_width_and_height(
+ image_info.size.width as f32 / dppx,
+ image_info.size.height as f32 / dppx,
+ );
+ let Some(image_key) = image_info.key else {
+ continue;
+ };
+
+ if let Some(layer) =
+ background::layout_layer(self, painter, builder, index, intrinsic)
+ {
+ if layer.repeat {
+ builder.wr().push_repeating_image(
+ &layer.common,
+ layer.bounds,
+ layer.tile_size,
+ layer.tile_spacing,
+ style.clone_image_rendering().to_webrender(),
+ wr::AlphaType::PremultipliedAlpha,
+ image_key,
+ wr::ColorF::WHITE,
+ )
+ } else {
+ builder.wr().push_image(
+ &layer.common,
+ layer.bounds,
+ style.clone_image_rendering().to_webrender(),
+ wr::AlphaType::PremultipliedAlpha,
+ image_key,
+ wr::ColorF::WHITE,
+ )
+ }
+ }
+ },
+ }
+ }
+ }
+
+ fn build_border_side(&mut self, style_color: BorderStyleColor) -> wr::BorderSide {
+ wr::BorderSide {
+ color: rgba(style_color.color),
+ style: match style_color.style {
+ BorderStyle::None => wr::BorderStyle::None,
+ BorderStyle::Solid => wr::BorderStyle::Solid,
+ BorderStyle::Double => wr::BorderStyle::Double,
+ BorderStyle::Dotted => wr::BorderStyle::Dotted,
+ BorderStyle::Dashed => wr::BorderStyle::Dashed,
+ BorderStyle::Hidden => wr::BorderStyle::Hidden,
+ BorderStyle::Groove => wr::BorderStyle::Groove,
+ BorderStyle::Ridge => wr::BorderStyle::Ridge,
+ BorderStyle::Inset => wr::BorderStyle::Inset,
+ BorderStyle::Outset => wr::BorderStyle::Outset,
+ },
+ }
+ }
+
+ fn build_collapsed_table_borders(&mut self, builder: &mut DisplayListBuilder) {
+ let Some(SpecificLayoutInfo::TableGridWithCollapsedBorders(table_info)) =
+ &self.fragment.specific_layout_info
+ else {
+ return;
+ };
+ let mut common =
+ builder.common_properties(units::LayoutRect::default(), &self.fragment.style);
+ let radius = wr::BorderRadius::default();
+ let mut column_sum = Au::zero();
+ for (x, column_size) in table_info.track_sizes.x.iter().enumerate() {
+ let mut row_sum = Au::zero();
+ for (y, row_size) in table_info.track_sizes.y.iter().enumerate() {
+ let left_border = &table_info.collapsed_borders.x[x][y];
+ let right_border = &table_info.collapsed_borders.x[x + 1][y];
+ let top_border = &table_info.collapsed_borders.y[y][x];
+ let bottom_border = &table_info.collapsed_borders.y[y + 1][x];
+ let details = wr::BorderDetails::Normal(wr::NormalBorder {
+ left: self.build_border_side(left_border.style_color.clone()),
+ right: self.build_border_side(right_border.style_color.clone()),
+ top: self.build_border_side(top_border.style_color.clone()),
+ bottom: self.build_border_side(bottom_border.style_color.clone()),
+ radius,
+ do_aa: true,
+ });
+ let mut border_widths = PhysicalSides::new(
+ top_border.width,
+ right_border.width,
+ bottom_border.width,
+ left_border.width,
+ );
+ let left_adjustment = if x == 0 {
+ -border_widths.left / 2
+ } else {
+ std::mem::take(&mut border_widths.left) / 2
+ };
+ let top_adjustment = if y == 0 {
+ -border_widths.top / 2
+ } else {
+ std::mem::take(&mut border_widths.top) / 2
+ };
+ let origin =
+ PhysicalPoint::new(column_sum + left_adjustment, row_sum + top_adjustment);
+ let size = PhysicalSize::new(
+ *column_size - left_adjustment + border_widths.right / 2,
+ *row_size - top_adjustment + border_widths.bottom / 2,
+ );
+ let border_rect = PhysicalRect::new(origin, size)
+ .translate(self.fragment.content_rect.origin.to_vector())
+ .translate(self.containing_block.origin.to_vector())
+ .to_webrender();
+ common.clip_rect = border_rect;
+ builder.wr().push_border(
+ &common,
+ border_rect,
+ border_widths.to_webrender(),
+ details,
+ );
+ row_sum += *row_size;
+ }
+ column_sum += *column_size;
+ }
+ }
+
+ fn build_border(&mut self, builder: &mut DisplayListBuilder) {
+ if self.fragment.has_collapsed_borders() {
+ // Avoid painting borders for tables and table parts in collapsed-borders mode,
+ // since the resulting collapsed borders are painted on their own in a special way.
+ return;
+ }
+
+ let border = self.fragment.style.get_border();
+ let border_widths = self.fragment.border.to_webrender();
+
+ if border_widths == SideOffsets2D::zero() {
+ return;
+ }
+
+ // `border-image` replaces an element's border entirely.
+ let common = builder.common_properties(self.border_rect, &self.fragment.style);
+ if self.build_border_image(builder, &common, border, border_widths) {
+ return;
+ }
+
+ let current_color = self.fragment.style.get_inherited_text().clone_color();
+ let style_color = BorderStyleColor::from_border(border, &current_color);
+ let details = wr::BorderDetails::Normal(wr::NormalBorder {
+ top: self.build_border_side(style_color.top),
+ right: self.build_border_side(style_color.right),
+ bottom: self.build_border_side(style_color.bottom),
+ left: self.build_border_side(style_color.left),
+ radius: self.border_radius,
+ do_aa: true,
+ });
+ builder
+ .wr()
+ .push_border(&common, self.border_rect, border_widths, details)
+ }
+
+ /// Add a display item for image borders if necessary.
+ fn build_border_image(
+ &self,
+ builder: &mut DisplayListBuilder,
+ common: &CommonItemProperties,
+ border: &Border,
+ border_widths: SideOffsets2D<f32, LayoutPixel>,
+ ) -> bool {
+ let border_style_struct = self.fragment.style.get_border();
+ let border_image_outset =
+ resolve_border_image_outset(border_style_struct.border_image_outset, border_widths);
+ let border_image_area = self.border_rect.to_rect().outer_rect(border_image_outset);
+ let border_image_size = border_image_area.size;
+ let border_image_widths = resolve_border_image_width(
+ &border_style_struct.border_image_width,
+ border_widths,
+ border_image_size,
+ );
+ let border_image_repeat = &border_style_struct.border_image_repeat;
+ let border_image_fill = border_style_struct.border_image_slice.fill;
+ let border_image_slice = &border_style_struct.border_image_slice.offsets;
+
+ let stops = Vec::new();
+ let mut width = border_image_size.width;
+ let mut height = border_image_size.height;
+ let node = self.fragment.base.tag.map(|tag| tag.node);
+ let source = match builder
+ .context
+ .resolve_image(node, &border.border_image_source)
+ {
+ None => return false,
+ Some(ResolvedImage::Image(image_info)) => {
+ let Some(key) = image_info.key else {
+ return false;
+ };
+
+ width = image_info.size.width as f32;
+ height = image_info.size.height as f32;
+ NinePatchBorderSource::Image(key, ImageRendering::Auto)
+ },
+ Some(ResolvedImage::Gradient(gradient)) => {
+ match gradient::build(&self.fragment.style, gradient, border_image_size, builder) {
+ WebRenderGradient::Linear(gradient) => {
+ NinePatchBorderSource::Gradient(gradient)
+ },
+ WebRenderGradient::Radial(gradient) => {
+ NinePatchBorderSource::RadialGradient(gradient)
+ },
+ WebRenderGradient::Conic(gradient) => {
+ NinePatchBorderSource::ConicGradient(gradient)
+ },
+ }
+ },
+ };
+
+ let size = euclid::Size2D::new(width as i32, height as i32);
+ let details = BorderDetails::NinePatch(NinePatchBorder {
+ source,
+ width: size.width,
+ height: size.height,
+ slice: resolve_border_image_slice(border_image_slice, size),
+ fill: border_image_fill,
+ repeat_horizontal: border_image_repeat.0.to_webrender(),
+ repeat_vertical: border_image_repeat.1.to_webrender(),
+ });
+ builder.wr().push_border(
+ common,
+ border_image_area.to_box2d(),
+ border_image_widths,
+ details,
+ );
+ builder.wr().push_stops(&stops);
+ true
+ }
+
+ fn build_outline(&mut self, builder: &mut DisplayListBuilder) {
+ let style = &self.fragment.style;
+ let outline = style.get_outline();
+ let width = outline.outline_width.to_f32_px();
+ if width == 0.0 {
+ return;
+ }
+ let offset = outline
+ .outline_offset
+ .px()
+ .max(-self.border_rect.width() / 2.0)
+ .max(-self.border_rect.height() / 2.0) +
+ width;
+ let outline_rect = self.border_rect.inflate(offset, offset);
+ let common = builder.common_properties(outline_rect, &self.fragment.style);
+ let widths = SideOffsets2D::new_all_same(width);
+ let border_style = match outline.outline_style {
+ // TODO: treating 'auto' as 'solid' is allowed by the spec,
+ // but we should do something better.
+ OutlineStyle::Auto => BorderStyle::Solid,
+ OutlineStyle::BorderStyle(s) => s,
+ };
+ let side = self.build_border_side(BorderStyleColor {
+ style: border_style,
+ color: style.resolve_color(&outline.outline_color),
+ });
+ let details = wr::BorderDetails::Normal(wr::NormalBorder {
+ top: side,
+ right: side,
+ bottom: side,
+ left: side,
+ radius: offset_radii(self.border_radius, offset),
+ do_aa: true,
+ });
+ builder
+ .wr()
+ .push_border(&common, outline_rect, widths, details)
+ }
+
+ fn build_box_shadow(&self, builder: &mut DisplayListBuilder<'_>) {
+ let box_shadows = &self.fragment.style.get_effects().box_shadow.0;
+ if box_shadows.is_empty() {
+ return;
+ }
+
+ // NB: According to CSS-BACKGROUNDS, box shadows render in *reverse* order (front to back).
+ let common = builder.common_properties(MaxRect::max_rect(), &self.fragment.style);
+ for box_shadow in box_shadows.iter().rev() {
+ let (rect, clip_mode) = if box_shadow.inset {
+ (*self.padding_rect(), BoxShadowClipMode::Inset)
+ } else {
+ (self.border_rect, BoxShadowClipMode::Outset)
+ };
+
+ builder.wr().push_box_shadow(
+ &common,
+ rect,
+ LayoutVector2D::new(
+ box_shadow.base.horizontal.px(),
+ box_shadow.base.vertical.px(),
+ ),
+ rgba(self.fragment.style.resolve_color(&box_shadow.base.color)),
+ box_shadow.base.blur.px(),
+ box_shadow.spread.px(),
+ self.border_radius,
+ clip_mode,
+ );
+ }
+ }
+}
+
+fn rgba(color: AbsoluteColor) -> wr::ColorF {
+ let rgba = color.to_color_space(ColorSpace::Srgb);
+ wr::ColorF::new(
+ rgba.components.0.clamp(0.0, 1.0),
+ rgba.components.1.clamp(0.0, 1.0),
+ rgba.components.2.clamp(0.0, 1.0),
+ rgba.alpha,
+ )
+}
+
+fn glyphs(
+ glyph_runs: &[Arc<GlyphStore>],
+ mut baseline_origin: PhysicalPoint<Au>,
+ justification_adjustment: Au,
+ ignore_whitespace: bool,
+) -> Vec<wr::GlyphInstance> {
+ use fonts_traits::ByteIndex;
+ use range::Range;
+
+ let mut glyphs = vec![];
+ for run in glyph_runs {
+ for glyph in run.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), run.len())) {
+ if !run.is_whitespace() || !ignore_whitespace {
+ let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
+ let point = units::LayoutPoint::new(
+ baseline_origin.x.to_f32_px() + glyph_offset.x.to_f32_px(),
+ baseline_origin.y.to_f32_px() + glyph_offset.y.to_f32_px(),
+ );
+ let glyph = wr::GlyphInstance {
+ index: glyph.id(),
+ point,
+ };
+ glyphs.push(glyph);
+ }
+
+ if glyph.char_is_word_separator() {
+ baseline_origin.x += justification_adjustment;
+ }
+ baseline_origin.x += glyph.advance();
+ }
+ }
+ glyphs
+}
+
+// TODO: The implementation here does not account for multiple glyph runs properly.
+fn glyphs_advance_by_index(
+ glyph_runs: &[Arc<GlyphStore>],
+ index: fonts_traits::ByteIndex,
+ baseline_origin: PhysicalPoint<Au>,
+ justification_adjustment: Au,
+) -> PhysicalPoint<Au> {
+ let mut point = baseline_origin;
+ let mut index = index;
+ for run in glyph_runs {
+ let range = ServoRange::new(fonts::ByteIndex(0), index.min(run.len()));
+ index = index - range.length();
+ let total_advance = run.advance_for_byte_range(&range, justification_adjustment);
+ point.x += total_advance;
+ }
+ point
+}
+
+fn cursor(kind: CursorKind, auto_cursor: Cursor) -> Cursor {
+ match kind {
+ CursorKind::Auto => auto_cursor,
+ CursorKind::None => Cursor::None,
+ CursorKind::Default => Cursor::Default,
+ CursorKind::Pointer => Cursor::Pointer,
+ CursorKind::ContextMenu => Cursor::ContextMenu,
+ CursorKind::Help => Cursor::Help,
+ CursorKind::Progress => Cursor::Progress,
+ CursorKind::Wait => Cursor::Wait,
+ CursorKind::Cell => Cursor::Cell,
+ CursorKind::Crosshair => Cursor::Crosshair,
+ CursorKind::Text => Cursor::Text,
+ CursorKind::VerticalText => Cursor::VerticalText,
+ CursorKind::Alias => Cursor::Alias,
+ CursorKind::Copy => Cursor::Copy,
+ CursorKind::Move => Cursor::Move,
+ CursorKind::NoDrop => Cursor::NoDrop,
+ CursorKind::NotAllowed => Cursor::NotAllowed,
+ CursorKind::Grab => Cursor::Grab,
+ CursorKind::Grabbing => Cursor::Grabbing,
+ CursorKind::EResize => Cursor::EResize,
+ CursorKind::NResize => Cursor::NResize,
+ CursorKind::NeResize => Cursor::NeResize,
+ CursorKind::NwResize => Cursor::NwResize,
+ CursorKind::SResize => Cursor::SResize,
+ CursorKind::SeResize => Cursor::SeResize,
+ CursorKind::SwResize => Cursor::SwResize,
+ CursorKind::WResize => Cursor::WResize,
+ CursorKind::EwResize => Cursor::EwResize,
+ CursorKind::NsResize => Cursor::NsResize,
+ CursorKind::NeswResize => Cursor::NeswResize,
+ CursorKind::NwseResize => Cursor::NwseResize,
+ CursorKind::ColResize => Cursor::ColResize,
+ CursorKind::RowResize => Cursor::RowResize,
+ CursorKind::AllScroll => Cursor::AllScroll,
+ CursorKind::ZoomIn => Cursor::ZoomIn,
+ CursorKind::ZoomOut => Cursor::ZoomOut,
+ }
+}
+
+/// Radii for the padding edge or content edge
+fn inner_radii(mut radii: wr::BorderRadius, insets: units::LayoutSideOffsets) -> wr::BorderRadius {
+ assert!(insets.left >= 0.0, "left inset must not be negative");
+ radii.top_left.width -= insets.left;
+ radii.bottom_left.width -= insets.left;
+
+ assert!(insets.right >= 0.0, "left inset must not be negative");
+ radii.top_right.width -= insets.right;
+ radii.bottom_right.width -= insets.right;
+
+ assert!(insets.top >= 0.0, "top inset must not be negative");
+ radii.top_left.height -= insets.top;
+ radii.top_right.height -= insets.top;
+
+ assert!(insets.bottom >= 0.0, "bottom inset must not be negative");
+ radii.bottom_left.height -= insets.bottom;
+ radii.bottom_right.height -= insets.bottom;
+ radii
+}
+
+fn offset_radii(mut radii: wr::BorderRadius, offset: f32) -> wr::BorderRadius {
+ if offset == 0.0 {
+ return radii;
+ }
+ if offset < 0.0 {
+ return inner_radii(radii, units::LayoutSideOffsets::new_all_same(-offset));
+ }
+ let expand = |radius: &mut f32| {
+ // Expand the radius by the specified amount, but keeping sharp corners.
+ // TODO: this behavior is not continuous, it's being discussed in the CSSWG:
+ // https://github.com/w3c/csswg-drafts/issues/7103
+ if *radius > 0.0 {
+ *radius += offset;
+ }
+ };
+ expand(&mut radii.top_left.width);
+ expand(&mut radii.top_left.height);
+ expand(&mut radii.top_right.width);
+ expand(&mut radii.top_right.height);
+ expand(&mut radii.bottom_right.width);
+ expand(&mut radii.bottom_right.height);
+ expand(&mut radii.bottom_left.width);
+ expand(&mut radii.bottom_left.height);
+ radii
+}
+
+fn create_clip_chain(
+ radii: wr::BorderRadius,
+ rect: units::LayoutRect,
+ builder: &mut DisplayListBuilder,
+ force_clip_creation: bool,
+) -> Option<ClipChainId> {
+ if radii.is_zero() && !force_clip_creation {
+ return None;
+ }
+
+ let spatial_id = builder.current_scroll_node_id.spatial_id;
+ let parent_clip_chain_id = builder.current_clip_chain_id;
+ let new_clip_id = if radii.is_zero() {
+ builder.wr().define_clip_rect(spatial_id, rect)
+ } else {
+ builder.wr().define_clip_rounded_rect(
+ spatial_id,
+ wr::ComplexClipRegion {
+ rect,
+ radii,
+ mode: wr::ClipMode::Clip,
+ },
+ )
+ };
+
+ Some(
+ builder
+ .display_list
+ .define_clip_chain(parent_clip_chain_id, [new_clip_id]),
+ )
+}
+
+/// Resolve the WebRender border-image outset area from the style values.
+fn resolve_border_image_outset(
+ outset: BorderImageOutset,
+ border: SideOffsets2D<f32, LayoutPixel>,
+) -> SideOffsets2D<f32, LayoutPixel> {
+ fn image_outset_for_side(outset: NonNegativeLengthOrNumber, border_width: f32) -> f32 {
+ match outset {
+ NonNegativeLengthOrNumber::Length(length) => length.px(),
+ NonNegativeLengthOrNumber::Number(factor) => border_width * factor.0,
+ }
+ }
+
+ SideOffsets2D::new(
+ image_outset_for_side(outset.0, border.top),
+ image_outset_for_side(outset.1, border.right),
+ image_outset_for_side(outset.2, border.bottom),
+ image_outset_for_side(outset.3, border.left),
+ )
+}
+
+/// Resolve the WebRender border-image width from the style values.
+fn resolve_border_image_width(
+ width: &BorderImageWidth,
+ border: SideOffsets2D<f32, LayoutPixel>,
+ border_area: Size2D<f32, LayoutPixel>,
+) -> SideOffsets2D<f32, LayoutPixel> {
+ fn image_width_for_side(
+ border_image_width: &BorderImageSideWidth,
+ border_width: f32,
+ total_length: f32,
+ ) -> f32 {
+ match border_image_width {
+ BorderImageSideWidth::LengthPercentage(v) => {
+ v.to_used_value(Au::from_f32_px(total_length)).to_f32_px()
+ },
+ BorderImageSideWidth::Number(x) => border_width * x.0,
+ BorderImageSideWidth::Auto => border_width,
+ }
+ }
+
+ SideOffsets2D::new(
+ image_width_for_side(&width.0, border.top, border_area.height),
+ image_width_for_side(&width.1, border.right, border_area.width),
+ image_width_for_side(&width.2, border.bottom, border_area.height),
+ image_width_for_side(&width.3, border.left, border_area.width),
+ )
+}
+
+/// Resolve the WebRender border-image slice from the style values.
+fn resolve_border_image_slice(
+ border_image_slice: &Rect<NonNegative<NumberOrPercentage>>,
+ size: Size2D<i32, UnknownUnit>,
+) -> SideOffsets2D<i32, DevicePixel> {
+ fn resolve_percentage(value: NonNegative<NumberOrPercentage>, length: i32) -> i32 {
+ match value.0 {
+ NumberOrPercentage::Percentage(p) => (p.0 * length as f32).round() as i32,
+ NumberOrPercentage::Number(n) => n.round() as i32,
+ }
+ }
+
+ SideOffsets2D::new(
+ resolve_percentage(border_image_slice.0, size.height),
+ resolve_percentage(border_image_slice.1, size.width),
+ resolve_percentage(border_image_slice.2, size.height),
+ resolve_percentage(border_image_slice.3, size.width),
+ )
+}
+
+pub(super) fn normalize_radii(rect: &units::LayoutRect, radius: &mut wr::BorderRadius) {
+ // Normalize radii that add up to > 100%.
+ // https://www.w3.org/TR/css-backgrounds-3/#corner-overlap
+ // > Let f = min(L_i/S_i), where i ∈ {top, right, bottom, left},
+ // > S_i is the sum of the two corresponding radii of the corners on side i,
+ // > and L_top = L_bottom = the width of the box,
+ // > and L_left = L_right = the height of the box.
+ // > If f < 1, then all corner radii are reduced by multiplying them by f.
+ let f = (rect.width() / (radius.top_left.width + radius.top_right.width))
+ .min(rect.width() / (radius.bottom_left.width + radius.bottom_right.width))
+ .min(rect.height() / (radius.top_left.height + radius.bottom_left.height))
+ .min(rect.height() / (radius.top_right.height + radius.bottom_right.height));
+ if f < 1.0 {
+ radius.top_left *= f;
+ radius.top_right *= f;
+ radius.bottom_right *= f;
+ radius.bottom_left *= f;
+ }
+}
+
+/// <https://drafts.csswg.org/css-shapes-1/#valdef-shape-box-margin-box>
+/// > The corner radii of this shape are determined by the corresponding
+/// > border-radius and margin values. If the ratio of border-radius/margin is 1 or more,
+/// > or margin is negative or zero, then the margin box corner radius is
+/// > max(border-radius + margin, 0). If the ratio of border-radius/margin is less than 1,
+/// > and margin is positive, then the margin box corner radius is
+/// > border-radius + margin * (1 + (ratio-1)^3).
+pub(super) fn compute_margin_box_radius(
+ radius: wr::BorderRadius,
+ layout_rect: LayoutSize,
+ fragment: &BoxFragment,
+) -> wr::BorderRadius {
+ let margin = fragment.style.physical_margin();
+ let adjust_radius = |radius: f32, margin: f32| -> f32 {
+ if margin <= 0. || (radius / margin) >= 1. {
+ (radius + margin).max(0.)
+ } else {
+ radius + (margin * (1. + (radius / margin - 1.).powf(3.)))
+ }
+ };
+ let compute_margin_radius = |radius: LayoutSize,
+ layout_rect: LayoutSize,
+ margin: Size2D<LengthPercentageOrAuto, UnknownUnit>|
+ -> LayoutSize {
+ let zero = LengthPercentage::zero();
+ let width = margin
+ .width
+ .auto_is(|| &zero)
+ .to_used_value(Au::from_f32_px(layout_rect.width));
+ let height = margin
+ .height
+ .auto_is(|| &zero)
+ .to_used_value(Au::from_f32_px(layout_rect.height));
+ LayoutSize::new(
+ adjust_radius(radius.width, width.to_f32_px()),
+ adjust_radius(radius.height, height.to_f32_px()),
+ )
+ };
+ wr::BorderRadius {
+ top_left: compute_margin_radius(
+ radius.top_left,
+ layout_rect,
+ Size2D::new(margin.left, margin.top),
+ ),
+ top_right: compute_margin_radius(
+ radius.top_right,
+ layout_rect,
+ Size2D::new(margin.right, margin.top),
+ ),
+ bottom_left: compute_margin_radius(
+ radius.bottom_left,
+ layout_rect,
+ Size2D::new(margin.left, margin.bottom),
+ ),
+ bottom_right: compute_margin_radius(
+ radius.bottom_right,
+ layout_rect,
+ Size2D::new(margin.right, margin.bottom),
+ ),
+ }
+}
diff --git a/components/layout/display_list/stacking_context.rs b/components/layout/display_list/stacking_context.rs
new file mode 100644
index 00000000000..0c0def9a563
--- /dev/null
+++ b/components/layout/display_list/stacking_context.rs
@@ -0,0 +1,1748 @@
+/* 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 core::f32;
+use std::cell::RefCell;
+use std::mem;
+
+use app_units::Au;
+use base::id::ScrollTreeNodeId;
+use base::print_tree::PrintTree;
+use compositing_traits::display_list::{AxesScrollSensitivity, ScrollableNodeInfo};
+use euclid::SideOffsets2D;
+use euclid::default::{Point2D, Rect, Size2D};
+use log::warn;
+use servo_arc::Arc as ServoArc;
+use servo_config::opts::DebugOptions;
+use style::Zero;
+use style::computed_values::float::T as ComputedFloat;
+use style::computed_values::mix_blend_mode::T as ComputedMixBlendMode;
+use style::computed_values::overflow_x::T as ComputedOverflow;
+use style::computed_values::position::T as ComputedPosition;
+use style::properties::ComputedValues;
+use style::values::computed::angle::Angle;
+use style::values::computed::basic_shape::ClipPath;
+use style::values::computed::{ClipRectOrAuto, Length};
+use style::values::generics::box_::Perspective;
+use style::values::generics::transform::{self, GenericRotate, GenericScale, GenericTranslate};
+use style::values::specified::box_::DisplayOutside;
+use webrender_api::units::{LayoutPoint, LayoutRect, LayoutTransform, LayoutVector2D};
+use webrender_api::{self as wr, BorderRadius};
+use wr::units::{LayoutPixel, LayoutSize};
+use wr::{ClipChainId, SpatialTreeItemKey, StickyOffsetBounds};
+
+use super::DisplayList;
+use super::clip_path::build_clip_path_clip_chain_if_necessary;
+use crate::display_list::conversions::{FilterToWebRender, ToWebRender};
+use crate::display_list::{BuilderForBoxFragment, DisplayListBuilder, offset_radii};
+use crate::fragment_tree::{
+ BoxFragment, ContainingBlockManager, Fragment, FragmentFlags, FragmentTree,
+ PositioningFragment, SpecificLayoutInfo,
+};
+use crate::geom::{AuOrAuto, PhysicalRect, PhysicalSides};
+use crate::style_ext::{ComputedValuesExt, TransformExt};
+
+#[derive(Clone)]
+pub(crate) struct ContainingBlock {
+ /// The SpatialId of the spatial node that contains the children
+ /// of this containing block.
+ scroll_node_id: ScrollTreeNodeId,
+
+ /// The size of the parent scroll frame of this containing block, used for resolving
+ /// sticky margins. If this is None, then this is a direct descendant of a reference
+ /// frame and sticky positioning isn't taken into account.
+ scroll_frame_size: Option<LayoutSize>,
+
+ /// The WebRender ClipId to use for this children of this containing
+ /// block.
+ clip_chain_id: wr::ClipChainId,
+
+ /// The physical rect of this containing block.
+ rect: PhysicalRect<Au>,
+}
+
+impl ContainingBlock {
+ pub(crate) fn new(
+ rect: PhysicalRect<Au>,
+ scroll_node_id: ScrollTreeNodeId,
+ scroll_frame_size: Option<LayoutSize>,
+ clip_chain_id: wr::ClipChainId,
+ ) -> Self {
+ ContainingBlock {
+ scroll_node_id,
+ scroll_frame_size,
+ clip_chain_id,
+ rect,
+ }
+ }
+
+ pub(crate) fn new_replacing_rect(&self, rect: &PhysicalRect<Au>) -> Self {
+ ContainingBlock {
+ rect: *rect,
+ ..*self
+ }
+ }
+}
+
+pub(crate) type ContainingBlockInfo<'a> = ContainingBlockManager<'a, ContainingBlock>;
+
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+pub(crate) enum StackingContextSection {
+ OwnBackgroundsAndBorders,
+ DescendantBackgroundsAndBorders,
+ Foreground,
+ Outline,
+}
+
+impl DisplayList {
+ /// Produce a new SpatialTreeItemKey. This is currently unused by WebRender,
+ /// but has to be unique to the entire scene.
+ fn get_next_spatial_tree_item_key(&mut self) -> SpatialTreeItemKey {
+ self.spatial_tree_count += 1;
+ let pipeline_tag = ((self.wr.pipeline_id.0 as u64) << 32) | self.wr.pipeline_id.1 as u64;
+ SpatialTreeItemKey::new(pipeline_tag, self.spatial_tree_count)
+ }
+
+ #[cfg_attr(
+ feature = "tracing",
+ tracing::instrument(
+ name = "display_list::build_stacking_context_tree",
+ skip_all,
+ fields(servo_profiling = true),
+ level = "trace",
+ )
+ )]
+ pub fn build_stacking_context_tree(
+ &mut self,
+ fragment_tree: &FragmentTree,
+ debug: &DebugOptions,
+ ) -> StackingContext {
+ let cb_for_non_fixed_descendants = ContainingBlock::new(
+ fragment_tree.initial_containing_block,
+ self.compositor_info.root_scroll_node_id,
+ Some(self.compositor_info.viewport_size),
+ ClipChainId::INVALID,
+ );
+ let cb_for_fixed_descendants = ContainingBlock::new(
+ fragment_tree.initial_containing_block,
+ self.compositor_info.root_reference_frame_id,
+ None,
+ ClipChainId::INVALID,
+ );
+
+ // We need to specify all three containing blocks here, because absolute
+ // descdendants of the root cannot share the containing block we specify
+ // for fixed descendants. In this case, they need to have the spatial
+ // id of the root scroll frame, whereas fixed descendants need the
+ // spatial id of the root reference frame so that they do not scroll with
+ // page content.
+ let containing_block_info = ContainingBlockInfo {
+ for_non_absolute_descendants: &cb_for_non_fixed_descendants,
+ for_absolute_descendants: Some(&cb_for_non_fixed_descendants),
+ for_absolute_and_fixed_descendants: &cb_for_fixed_descendants,
+ };
+
+ let mut root_stacking_context = StackingContext::create_root(&self.wr, debug);
+ for fragment in &fragment_tree.root_fragments {
+ fragment.build_stacking_context_tree(
+ self,
+ &containing_block_info,
+ &mut root_stacking_context,
+ StackingContextBuildMode::SkipHoisted,
+ );
+ }
+ root_stacking_context.sort();
+ root_stacking_context
+ }
+
+ fn push_reference_frame(
+ &mut self,
+ origin: LayoutPoint,
+ parent_scroll_node_id: &ScrollTreeNodeId,
+ transform_style: wr::TransformStyle,
+ transform: wr::PropertyBinding<LayoutTransform>,
+ kind: wr::ReferenceFrameKind,
+ ) -> ScrollTreeNodeId {
+ let spatial_tree_item_key = self.get_next_spatial_tree_item_key();
+ let new_spatial_id = self.wr.push_reference_frame(
+ origin,
+ parent_scroll_node_id.spatial_id,
+ transform_style,
+ transform,
+ kind,
+ spatial_tree_item_key,
+ );
+ self.compositor_info.scroll_tree.add_scroll_tree_node(
+ Some(parent_scroll_node_id),
+ new_spatial_id,
+ None,
+ )
+ }
+
+ fn pop_reference_frame(&mut self) {
+ self.wr.pop_reference_frame();
+ }
+
+ fn clip_overflow_frame(
+ &mut self,
+ parent_scroll_node_id: &ScrollTreeNodeId,
+ parent_clip_id: &ClipChainId,
+ clip_rect: LayoutRect,
+ radii: wr::BorderRadius,
+ ) -> ClipChainId {
+ let new_clip_id = if radii.is_zero() {
+ self.wr
+ .define_clip_rect(parent_scroll_node_id.spatial_id, clip_rect)
+ } else {
+ self.wr.define_clip_rounded_rect(
+ parent_scroll_node_id.spatial_id,
+ webrender_api::ComplexClipRegion {
+ rect: clip_rect,
+ radii,
+ mode: webrender_api::ClipMode::Clip,
+ },
+ )
+ };
+
+ self.define_clip_chain(*parent_clip_id, [new_clip_id])
+ }
+
+ fn define_scroll_frame(
+ &mut self,
+ parent_scroll_node_id: &ScrollTreeNodeId,
+ external_id: wr::ExternalScrollId,
+ content_rect: LayoutRect,
+ clip_rect: LayoutRect,
+ scroll_sensitivity: AxesScrollSensitivity,
+ ) -> ScrollTreeNodeId {
+ let spatial_tree_item_key = self.get_next_spatial_tree_item_key();
+
+ let new_spatial_id = self.wr.define_scroll_frame(
+ parent_scroll_node_id.spatial_id,
+ external_id,
+ content_rect,
+ clip_rect,
+ LayoutVector2D::zero(), /* external_scroll_offset */
+ 0, /* scroll_offset_generation */
+ wr::HasScrollLinkedEffect::No,
+ spatial_tree_item_key,
+ );
+
+ self.compositor_info.scroll_tree.add_scroll_tree_node(
+ Some(parent_scroll_node_id),
+ new_spatial_id,
+ Some(ScrollableNodeInfo {
+ external_id,
+ scrollable_size: content_rect.size() - clip_rect.size(),
+ scroll_sensitivity,
+ offset: LayoutVector2D::zero(),
+ }),
+ )
+ }
+
+ fn define_sticky_frame(
+ &mut self,
+ parent_scroll_node_id: &ScrollTreeNodeId,
+ frame_rect: LayoutRect,
+ margins: SideOffsets2D<Option<f32>, LayoutPixel>,
+ vertical_offset_bounds: StickyOffsetBounds,
+ horizontal_offset_bounds: StickyOffsetBounds,
+ ) -> ScrollTreeNodeId {
+ let spatial_tree_item_key = self.get_next_spatial_tree_item_key();
+ let new_spatial_id = self.wr.define_sticky_frame(
+ parent_scroll_node_id.spatial_id,
+ frame_rect,
+ margins,
+ vertical_offset_bounds,
+ horizontal_offset_bounds,
+ LayoutVector2D::zero(), /* previously_applied_offset */
+ spatial_tree_item_key,
+ None, /* transform */
+ );
+ self.compositor_info.scroll_tree.add_scroll_tree_node(
+ Some(parent_scroll_node_id),
+ new_spatial_id,
+ None,
+ )
+ }
+}
+
+/// A piece of content that directly belongs to a section of a stacking context.
+///
+/// This is generally part of a fragment, like its borders or foreground, but it
+/// can also be a stacking container that needs to be painted in fragment order.
+pub(crate) enum StackingContextContent {
+ /// A fragment that does not generate a stacking context or stacking container.
+ Fragment {
+ scroll_node_id: ScrollTreeNodeId,
+ reference_frame_scroll_node_id: ScrollTreeNodeId,
+ clip_chain_id: wr::ClipChainId,
+ section: StackingContextSection,
+ containing_block: PhysicalRect<Au>,
+ fragment: Fragment,
+ is_hit_test_for_scrollable_overflow: bool,
+ is_collapsed_table_borders: bool,
+ },
+
+ /// An index into [StackingContext::atomic_inline_stacking_containers].
+ ///
+ /// There is no section field, because these are always in [StackingContextSection::Foreground].
+ AtomicInlineStackingContainer { index: usize },
+}
+
+impl StackingContextContent {
+ fn section(&self) -> StackingContextSection {
+ match self {
+ Self::Fragment { section, .. } => *section,
+ Self::AtomicInlineStackingContainer { .. } => StackingContextSection::Foreground,
+ }
+ }
+
+ fn build_display_list(
+ &self,
+ builder: &mut DisplayListBuilder,
+ inline_stacking_containers: &[StackingContext],
+ ) {
+ match self {
+ Self::Fragment {
+ scroll_node_id,
+ reference_frame_scroll_node_id,
+ clip_chain_id,
+ section,
+ containing_block,
+ fragment,
+ is_hit_test_for_scrollable_overflow,
+ is_collapsed_table_borders,
+ } => {
+ builder.current_scroll_node_id = *scroll_node_id;
+ builder.current_reference_frame_scroll_node_id = *reference_frame_scroll_node_id;
+ builder.current_clip_chain_id = *clip_chain_id;
+ fragment.build_display_list(
+ builder,
+ containing_block,
+ *section,
+ *is_hit_test_for_scrollable_overflow,
+ *is_collapsed_table_borders,
+ );
+ },
+ Self::AtomicInlineStackingContainer { index } => {
+ inline_stacking_containers[*index].build_display_list(builder);
+ },
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub(crate) enum StackingContextType {
+ RealStackingContext,
+ PositionedStackingContainer,
+ FloatStackingContainer,
+ AtomicInlineStackingContainer,
+}
+
+/// Either a stacking context or a stacking container, per the definitions in
+/// <https://drafts.csswg.org/css-position-4/#painting-order>.
+///
+/// We use the term “real stacking context” in situations that call for a
+/// stacking context but not a stacking container.
+pub struct StackingContext {
+ /// The spatial id of this fragment. This is used to properly handle
+ /// things like preserve-3d.
+ spatial_id: wr::SpatialId,
+
+ /// The clip chain id of this stacking context if it has one. Used for filter clipping.
+ clip_chain_id: Option<wr::ClipChainId>,
+
+ /// The style of the fragment that established this stacking context.
+ initializing_fragment_style: Option<ServoArc<ComputedValues>>,
+
+ /// The [`FragmentFlags`] of the [`Fragment`] that established this stacking context.
+ initializing_fragment_flags: FragmentFlags,
+
+ /// The type of this stacking context. Used for collecting and sorting.
+ context_type: StackingContextType,
+
+ /// The contents that need to be painted in fragment order.
+ contents: Vec<StackingContextContent>,
+
+ /// Stacking contexts that need to be stolen by the parent stacking context
+ /// if this is a stacking container, that is, real stacking contexts and
+ /// positioned stacking containers (where ‘z-index’ is auto).
+ /// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-container>
+ /// > To paint a stacking container, given a box root and a canvas canvas:
+ /// > 1. Paint a stacking context given root and canvas, treating root as
+ /// > if it created a new stacking context, but omitting any positioned
+ /// > descendants or descendants that actually create a stacking context
+ /// > (letting the parent stacking context paint them, instead).
+ real_stacking_contexts_and_positioned_stacking_containers: Vec<StackingContext>,
+
+ /// Float stacking containers.
+ /// Separate from real_stacking_contexts_or_positioned_stacking_containers
+ /// because they should never be stolen by the parent stacking context.
+ /// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-container>
+ float_stacking_containers: Vec<StackingContext>,
+
+ /// Atomic inline stacking containers.
+ /// Separate from real_stacking_contexts_or_positioned_stacking_containers
+ /// because they should never be stolen by the parent stacking context, and
+ /// separate from float_stacking_containers so that [StackingContextContent]
+ /// can index into this vec to paint them in fragment order.
+ /// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-container>
+ /// <https://drafts.csswg.org/css-position-4/#paint-a-box-in-a-line-box>
+ atomic_inline_stacking_containers: Vec<StackingContext>,
+
+ /// Information gathered about the painting order, for [Self::debug_print].
+ debug_print_items: Option<RefCell<Vec<DebugPrintItem>>>,
+}
+
+/// Refers to one of the child contents or stacking contexts of a [StackingContext].
+#[derive(Clone, Copy)]
+pub struct DebugPrintItem {
+ field: DebugPrintField,
+ index: usize,
+}
+
+/// Refers to one of the vecs of a [StackingContext].
+#[derive(Clone, Copy)]
+pub enum DebugPrintField {
+ Contents,
+ RealStackingContextsAndPositionedStackingContainers,
+ FloatStackingContainers,
+ AtomicInlineStackingContainers,
+}
+
+impl StackingContext {
+ fn create_descendant(
+ &self,
+ spatial_id: wr::SpatialId,
+ clip_chain_id: wr::ClipChainId,
+ initializing_fragment_style: ServoArc<ComputedValues>,
+ initializing_fragment_flags: FragmentFlags,
+ context_type: StackingContextType,
+ ) -> Self {
+ // WebRender has two different ways of expressing "no clip." ClipChainId::INVALID should be
+ // used for primitives, but `None` is used for stacking contexts and clip chains. We convert
+ // to the `Option<ClipChainId>` representation here. Just passing Some(ClipChainId::INVALID)
+ // leads to a crash.
+ let clip_chain_id: Option<ClipChainId> = match clip_chain_id {
+ ClipChainId::INVALID => None,
+ clip_chain_id => Some(clip_chain_id),
+ };
+ Self {
+ spatial_id,
+ clip_chain_id,
+ initializing_fragment_style: Some(initializing_fragment_style),
+ initializing_fragment_flags,
+ context_type,
+ contents: vec![],
+ real_stacking_contexts_and_positioned_stacking_containers: vec![],
+ float_stacking_containers: vec![],
+ atomic_inline_stacking_containers: vec![],
+ debug_print_items: self.debug_print_items.is_some().then(|| vec![].into()),
+ }
+ }
+
+ pub(crate) fn create_root(wr: &wr::DisplayListBuilder, debug: &DebugOptions) -> Self {
+ Self {
+ spatial_id: wr::SpaceAndClipInfo::root_scroll(wr.pipeline_id).spatial_id,
+ clip_chain_id: None,
+ initializing_fragment_style: None,
+ initializing_fragment_flags: FragmentFlags::empty(),
+ context_type: StackingContextType::RealStackingContext,
+ contents: vec![],
+ real_stacking_contexts_and_positioned_stacking_containers: vec![],
+ float_stacking_containers: vec![],
+ atomic_inline_stacking_containers: vec![],
+ debug_print_items: debug.dump_stacking_context_tree.then(|| vec![].into()),
+ }
+ }
+
+ /// Add a child stacking context to this stacking context.
+ fn add_stacking_context(&mut self, stacking_context: StackingContext) {
+ match stacking_context.context_type {
+ StackingContextType::RealStackingContext => {
+ &mut self.real_stacking_contexts_and_positioned_stacking_containers
+ },
+ StackingContextType::PositionedStackingContainer => {
+ &mut self.real_stacking_contexts_and_positioned_stacking_containers
+ },
+ StackingContextType::FloatStackingContainer => &mut self.float_stacking_containers,
+ StackingContextType::AtomicInlineStackingContainer => {
+ &mut self.atomic_inline_stacking_containers
+ },
+ }
+ .push(stacking_context)
+ }
+
+ fn z_index(&self) -> i32 {
+ self.initializing_fragment_style
+ .as_ref()
+ .map_or(0, |style| {
+ style.effective_z_index(self.initializing_fragment_flags)
+ })
+ }
+
+ pub(crate) fn sort(&mut self) {
+ self.contents.sort_by_key(|a| a.section());
+ self.real_stacking_contexts_and_positioned_stacking_containers
+ .sort_by_key(|a| a.z_index());
+
+ debug_assert!(
+ self.real_stacking_contexts_and_positioned_stacking_containers
+ .iter()
+ .all(|c| matches!(
+ c.context_type,
+ StackingContextType::RealStackingContext |
+ StackingContextType::PositionedStackingContainer
+ ))
+ );
+ debug_assert!(
+ self.float_stacking_containers
+ .iter()
+ .all(
+ |c| c.context_type == StackingContextType::FloatStackingContainer &&
+ c.z_index() == 0
+ )
+ );
+ debug_assert!(
+ self.atomic_inline_stacking_containers
+ .iter()
+ .all(
+ |c| c.context_type == StackingContextType::AtomicInlineStackingContainer &&
+ c.z_index() == 0
+ )
+ );
+ }
+
+ fn push_webrender_stacking_context_if_necessary(
+ &self,
+ builder: &mut DisplayListBuilder,
+ ) -> bool {
+ let style = match self.initializing_fragment_style.as_ref() {
+ Some(style) => style,
+ None => return false,
+ };
+
+ // WebRender only uses the stacking context to apply certain effects. If we don't
+ // actually need to create a stacking context, just avoid creating one.
+ let effects = style.get_effects();
+ if effects.filter.0.is_empty() &&
+ effects.opacity == 1.0 &&
+ effects.mix_blend_mode == ComputedMixBlendMode::Normal &&
+ !style.has_transform_or_perspective(FragmentFlags::empty()) &&
+ style.clone_clip_path() == ClipPath::None
+ {
+ return false;
+ }
+
+ // Create the filter pipeline.
+ let current_color = style.clone_color();
+ let mut filters: Vec<wr::FilterOp> = effects
+ .filter
+ .0
+ .iter()
+ .map(|filter| FilterToWebRender::to_webrender(filter, &current_color))
+ .collect();
+ if effects.opacity != 1.0 {
+ filters.push(wr::FilterOp::Opacity(
+ effects.opacity.into(),
+ effects.opacity,
+ ));
+ }
+
+ // TODO(jdm): WebRender now requires us to create stacking context items
+ // with the IS_BLEND_CONTAINER flag enabled if any children
+ // of the stacking context have a blend mode applied.
+ // This will require additional tracking during layout
+ // before we start collecting stacking contexts so that
+ // information will be available when we reach this point.
+ builder.wr().push_stacking_context(
+ LayoutPoint::zero(), // origin
+ self.spatial_id,
+ style.get_webrender_primitive_flags(),
+ self.clip_chain_id,
+ style.get_used_transform_style().to_webrender(),
+ effects.mix_blend_mode.to_webrender(),
+ &filters,
+ &[], // filter_datas
+ &[], // filter_primitives
+ wr::RasterSpace::Screen,
+ wr::StackingContextFlags::empty(),
+ None, // snapshot
+ );
+
+ true
+ }
+
+ /// <https://drafts.csswg.org/css-backgrounds/#special-backgrounds>
+ ///
+ /// This is only called for the root `StackingContext`
+ pub(crate) fn build_canvas_background_display_list(
+ &self,
+ builder: &mut DisplayListBuilder,
+ fragment_tree: &crate::FragmentTree,
+ containing_block_rect: &PhysicalRect<Au>,
+ ) {
+ let style = if let Some(style) = &fragment_tree.canvas_background.style {
+ style
+ } else {
+ // The root element has `display: none`,
+ // or the canvas background is taken from `<body>` which has `display: none`
+ return;
+ };
+
+ // The painting area is theoretically the infinite 2D plane,
+ // but we need a rectangle with finite coordinates.
+ //
+ // If the document is smaller than the viewport (and doesn’t scroll),
+ // we still want to paint the rest of the viewport.
+ // If it’s larger, we also want to paint areas reachable after scrolling.
+ let mut painting_area = fragment_tree
+ .initial_containing_block
+ .union(&fragment_tree.scrollable_overflow)
+ .to_webrender();
+
+ let background_color = style.resolve_color(&style.get_background().background_color);
+ if background_color.alpha > 0.0 {
+ let common = builder.common_properties(painting_area, style);
+ let color = super::rgba(background_color);
+ builder
+ .display_list
+ .wr
+ .push_rect(&common, painting_area, color)
+ }
+
+ // `background-color` was comparatively easy,
+ // but `background-image` needs a positioning area based on the root element.
+ // Let’s find the corresponding fragment.
+
+ // The fragment generated by the root element is the first one here, unless…
+ let first_if_any = self.contents.first().or_else(|| {
+ // There wasn’t any `StackingContextFragment` in the root `StackingContext`,
+ // because the root element generates a stacking context. Let’s find that one.
+ self.real_stacking_contexts_and_positioned_stacking_containers
+ .first()
+ .and_then(|first_child_stacking_context| {
+ first_child_stacking_context.contents.first()
+ })
+ });
+
+ macro_rules! debug_panic {
+ ($msg: expr) => {
+ if cfg!(debug_assertions) {
+ panic!($msg);
+ } else {
+ warn!($msg);
+ return;
+ }
+ };
+ }
+
+ let first_stacking_context_fragment = if let Some(first) = first_if_any {
+ first
+ } else {
+ // This should only happen if the root element has `display: none`
+ // TODO(servo#30569) revert to debug_panic!() once underlying bug is fixed
+ log::warn!(
+ "debug assertion failed! `CanvasBackground::for_root_element` should have returned `style: None`",
+ );
+ return;
+ };
+
+ let StackingContextContent::Fragment {
+ fragment,
+ scroll_node_id,
+ containing_block,
+ ..
+ } = first_stacking_context_fragment
+ else {
+ debug_panic!("Expected a fragment, not a stacking container");
+ };
+ let box_fragment = match fragment {
+ Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => box_fragment,
+ _ => debug_panic!("Expected a box-generated fragment"),
+ };
+ let box_fragment = &*box_fragment.borrow();
+
+ // The `StackingContextFragment` we found is for the root DOM element:
+ debug_assert_eq!(
+ fragment.tag().map(|tag| tag.node),
+ Some(fragment_tree.canvas_background.root_element),
+ );
+
+ // The root element may have a CSS transform, and we want the canvas’
+ // background image to be transformed. To do so, take its `SpatialId`
+ // (but not its `ClipId`)
+ builder.current_scroll_node_id = *scroll_node_id;
+
+ // Now we need express the painting area rectangle in the local coordinate system,
+ // which differs from the top-level coordinate system based on…
+
+ // Convert the painting area rectangle to the local coordinate system of this `SpatialId`
+ if let Some(reference_frame_data) =
+ box_fragment.reference_frame_data_if_necessary(containing_block_rect)
+ {
+ painting_area.min -= reference_frame_data.origin.to_webrender().to_vector();
+ if let Some(transformed) = reference_frame_data
+ .transform
+ .inverse()
+ .and_then(|inversed| inversed.outer_transformed_rect(&painting_area.to_rect()))
+ {
+ painting_area = transformed.to_box2d();
+ } else {
+ // The desired rect cannot be represented, so skip painting this background-image
+ return;
+ }
+ }
+
+ let mut fragment_builder = BuilderForBoxFragment::new(
+ box_fragment,
+ containing_block,
+ false, /* is_hit_test_for_scrollable_overflow */
+ false, /* is_collapsed_table_borders */
+ );
+ let painter = super::background::BackgroundPainter {
+ style,
+ painting_area_override: Some(painting_area),
+ positioning_area_override: None,
+ };
+ fragment_builder.build_background_image(builder, &painter);
+ }
+
+ pub(crate) fn build_display_list(&self, builder: &mut DisplayListBuilder) {
+ let pushed_context = self.push_webrender_stacking_context_if_necessary(builder);
+
+ // Properly order display items that make up a stacking context.
+ // “Steps” here refer to the steps in CSS 2.1 Appendix E.
+ // Note that “positioned descendants” is generalised to include all descendants that
+ // generate stacking contexts (csswg-drafts#2717), except in the phrase “any positioned
+ // descendants or descendants that actually create a stacking context”, where the term
+ // means positioned descendants that do not generate stacking contexts.
+
+ // Steps 1 and 2: Borders and background for the root
+ let mut contents = self.contents.iter().enumerate().peekable();
+ while contents.peek().is_some_and(|(_, child)| {
+ child.section() == StackingContextSection::OwnBackgroundsAndBorders
+ }) {
+ let (i, child) = contents.next().unwrap();
+ self.debug_push_print_item(DebugPrintField::Contents, i);
+ child.build_display_list(builder, &self.atomic_inline_stacking_containers);
+ }
+
+ // Step 3: Stacking contexts with negative ‘z-index’
+ let mut real_stacking_contexts_and_positioned_stacking_containers = self
+ .real_stacking_contexts_and_positioned_stacking_containers
+ .iter()
+ .enumerate()
+ .peekable();
+ while real_stacking_contexts_and_positioned_stacking_containers
+ .peek()
+ .is_some_and(|(_, child)| child.z_index() < 0)
+ {
+ let (i, child) = real_stacking_contexts_and_positioned_stacking_containers
+ .next()
+ .unwrap();
+ self.debug_push_print_item(
+ DebugPrintField::RealStackingContextsAndPositionedStackingContainers,
+ i,
+ );
+ child.build_display_list(builder);
+ }
+
+ // Step 4: Block backgrounds and borders
+ while contents.peek().is_some_and(|(_, child)| {
+ child.section() == StackingContextSection::DescendantBackgroundsAndBorders
+ }) {
+ let (i, child) = contents.next().unwrap();
+ self.debug_push_print_item(DebugPrintField::Contents, i);
+ child.build_display_list(builder, &self.atomic_inline_stacking_containers);
+ }
+
+ // Step 5: Float stacking containers
+ for (i, child) in self.float_stacking_containers.iter().enumerate() {
+ self.debug_push_print_item(DebugPrintField::FloatStackingContainers, i);
+ child.build_display_list(builder);
+ }
+
+ // Steps 6 and 7: Fragments and inline stacking containers
+ while contents
+ .peek()
+ .is_some_and(|(_, child)| child.section() == StackingContextSection::Foreground)
+ {
+ let (i, child) = contents.next().unwrap();
+ self.debug_push_print_item(DebugPrintField::Contents, i);
+ child.build_display_list(builder, &self.atomic_inline_stacking_containers);
+ }
+
+ // Steps 8 and 9: Stacking contexts with non-negative ‘z-index’, and
+ // positioned stacking containers (where ‘z-index’ is auto)
+ for (i, child) in real_stacking_contexts_and_positioned_stacking_containers {
+ self.debug_push_print_item(
+ DebugPrintField::RealStackingContextsAndPositionedStackingContainers,
+ i,
+ );
+ child.build_display_list(builder);
+ }
+
+ // Step 10: Outline
+ while contents
+ .peek()
+ .is_some_and(|(_, child)| child.section() == StackingContextSection::Outline)
+ {
+ let (i, child) = contents.next().unwrap();
+ self.debug_push_print_item(DebugPrintField::Contents, i);
+ child.build_display_list(builder, &self.atomic_inline_stacking_containers);
+ }
+
+ if pushed_context {
+ builder.display_list.wr.pop_stacking_context();
+ }
+ }
+
+ /// Store the fact that something was painted, if [Self::debug_print_items] is not None.
+ ///
+ /// This is used to help reconstruct the original painting order in [Self::debug_print] without
+ /// duplicating our painting order logic, since that could fall out of sync with the real logic.
+ fn debug_push_print_item(&self, field: DebugPrintField, index: usize) {
+ if let Some(items) = self.debug_print_items.as_ref() {
+ items.borrow_mut().push(DebugPrintItem { field, index });
+ }
+ }
+
+ /// Print the stacking context tree.
+ pub fn debug_print(&self) {
+ if self.debug_print_items.is_none() {
+ warn!("failed to print stacking context tree: debug_print_items was None");
+ return;
+ }
+ let mut tree = PrintTree::new("Stacking context tree".to_owned());
+ self.debug_print_with_tree(&mut tree);
+ }
+
+ /// Print a subtree with the given [PrintTree], or panic if [Self::debug_print_items] is None.
+ fn debug_print_with_tree(&self, tree: &mut PrintTree) {
+ match self.context_type {
+ StackingContextType::RealStackingContext => {
+ tree.new_level(format!("{:?} z={}", self.context_type, self.z_index()));
+ },
+ StackingContextType::AtomicInlineStackingContainer => {
+ // do nothing; we print the heading with its index in DebugPrintField::Contents
+ },
+ _ => {
+ tree.new_level(format!("{:?}", self.context_type));
+ },
+ }
+ for DebugPrintItem { field, index } in
+ self.debug_print_items.as_ref().unwrap().borrow().iter()
+ {
+ match field {
+ DebugPrintField::Contents => match self.contents[*index] {
+ StackingContextContent::Fragment { section, .. } => {
+ tree.add_item(format!("{section:?}"));
+ },
+ StackingContextContent::AtomicInlineStackingContainer { index } => {
+ tree.new_level(format!("AtomicInlineStackingContainer #{index}"));
+ self.atomic_inline_stacking_containers[index].debug_print_with_tree(tree);
+ tree.end_level();
+ },
+ },
+ DebugPrintField::RealStackingContextsAndPositionedStackingContainers => {
+ self.real_stacking_contexts_and_positioned_stacking_containers[*index]
+ .debug_print_with_tree(tree);
+ },
+ DebugPrintField::FloatStackingContainers => {
+ self.float_stacking_containers[*index].debug_print_with_tree(tree);
+ },
+ DebugPrintField::AtomicInlineStackingContainers => {
+ // do nothing; we print these in DebugPrintField::Contents
+ },
+ }
+ }
+ match self.context_type {
+ StackingContextType::AtomicInlineStackingContainer => {
+ // do nothing; we print the heading with its index in DebugPrintField::Contents
+ },
+ _ => {
+ tree.end_level();
+ },
+ }
+ }
+}
+
+#[derive(PartialEq)]
+pub(crate) enum StackingContextBuildMode {
+ IncludeHoisted,
+ SkipHoisted,
+}
+
+impl Fragment {
+ pub(crate) fn build_stacking_context_tree(
+ &self,
+ display_list: &mut DisplayList,
+ containing_block_info: &ContainingBlockInfo,
+ stacking_context: &mut StackingContext,
+ mode: StackingContextBuildMode,
+ ) {
+ let containing_block = containing_block_info.get_containing_block_for_fragment(self);
+ let fragment_clone = self.clone();
+ match self {
+ Fragment::Box(fragment) | Fragment::Float(fragment) => {
+ let fragment = fragment.borrow();
+ if mode == StackingContextBuildMode::SkipHoisted &&
+ fragment.style.clone_position().is_absolutely_positioned()
+ {
+ return;
+ }
+
+ // If this fragment has a transform applied that makes it take up no space
+ // then we don't need to create any stacking contexts for it.
+ let has_non_invertible_transform = fragment
+ .has_non_invertible_transform_or_zero_scale(
+ &containing_block.rect.to_untyped(),
+ );
+ if has_non_invertible_transform {
+ return;
+ }
+
+ fragment.build_stacking_context_tree(
+ fragment_clone,
+ display_list,
+ containing_block,
+ containing_block_info,
+ stacking_context,
+ );
+ },
+ Fragment::AbsoluteOrFixedPositioned(fragment) => {
+ let shared_fragment = fragment.borrow();
+ let fragment_ref = match shared_fragment.fragment.as_ref() {
+ Some(fragment_ref) => fragment_ref,
+ None => unreachable!("Found hoisted box with missing fragment."),
+ };
+
+ fragment_ref.build_stacking_context_tree(
+ display_list,
+ containing_block_info,
+ stacking_context,
+ StackingContextBuildMode::IncludeHoisted,
+ );
+ },
+ Fragment::Positioning(fragment) => {
+ let fragment = fragment.borrow();
+ fragment.build_stacking_context_tree(
+ display_list,
+ containing_block,
+ containing_block_info,
+ stacking_context,
+ );
+ },
+ Fragment::Text(_) | Fragment::Image(_) | Fragment::IFrame(_) => {
+ stacking_context
+ .contents
+ .push(StackingContextContent::Fragment {
+ section: StackingContextSection::Foreground,
+ scroll_node_id: containing_block.scroll_node_id,
+ reference_frame_scroll_node_id: containing_block_info
+ .for_absolute_and_fixed_descendants
+ .scroll_node_id,
+ clip_chain_id: containing_block.clip_chain_id,
+ containing_block: containing_block.rect,
+ fragment: fragment_clone,
+ is_hit_test_for_scrollable_overflow: false,
+ is_collapsed_table_borders: false,
+ });
+ },
+ }
+ }
+}
+
+struct ReferenceFrameData {
+ origin: crate::geom::PhysicalPoint<Au>,
+ transform: LayoutTransform,
+ kind: wr::ReferenceFrameKind,
+}
+struct ScrollFrameData {
+ scroll_tree_node_id: ScrollTreeNodeId,
+ scroll_frame_rect: LayoutRect,
+}
+
+struct OverflowFrameData {
+ clip_chain_id: wr::ClipChainId,
+ scroll_frame_data: Option<ScrollFrameData>,
+}
+
+impl BoxFragment {
+ fn get_stacking_context_type(&self) -> Option<StackingContextType> {
+ if self.style.establishes_stacking_context(self.base.flags) {
+ return Some(StackingContextType::RealStackingContext);
+ }
+
+ let box_style = &self.style.get_box();
+ if box_style.position != ComputedPosition::Static {
+ return Some(StackingContextType::PositionedStackingContainer);
+ }
+
+ if box_style.float != ComputedFloat::None {
+ return Some(StackingContextType::FloatStackingContainer);
+ }
+
+ if self.is_atomic_inline_level() {
+ return Some(StackingContextType::AtomicInlineStackingContainer);
+ }
+
+ None
+ }
+
+ fn get_stacking_context_section(&self) -> StackingContextSection {
+ if self.get_stacking_context_type().is_some() {
+ return StackingContextSection::OwnBackgroundsAndBorders;
+ }
+
+ if self.style.get_box().display.outside() == DisplayOutside::Inline {
+ return StackingContextSection::Foreground;
+ }
+
+ StackingContextSection::DescendantBackgroundsAndBorders
+ }
+
+ fn build_stacking_context_tree(
+ &self,
+ fragment: Fragment,
+ display_list: &mut DisplayList,
+ containing_block: &ContainingBlock,
+ containing_block_info: &ContainingBlockInfo,
+ parent_stacking_context: &mut StackingContext,
+ ) {
+ self.build_stacking_context_tree_maybe_creating_reference_frame(
+ fragment,
+ display_list,
+ containing_block,
+ containing_block_info,
+ parent_stacking_context,
+ );
+ }
+
+ fn build_stacking_context_tree_maybe_creating_reference_frame(
+ &self,
+ fragment: Fragment,
+ display_list: &mut DisplayList,
+ containing_block: &ContainingBlock,
+ containing_block_info: &ContainingBlockInfo,
+ parent_stacking_context: &mut StackingContext,
+ ) {
+ let reference_frame_data =
+ match self.reference_frame_data_if_necessary(&containing_block.rect) {
+ Some(reference_frame_data) => reference_frame_data,
+ None => {
+ return self.build_stacking_context_tree_maybe_creating_stacking_context(
+ fragment,
+ display_list,
+ containing_block,
+ containing_block_info,
+ parent_stacking_context,
+ );
+ },
+ };
+
+ let new_spatial_id = display_list.push_reference_frame(
+ reference_frame_data.origin.to_webrender(),
+ &containing_block.scroll_node_id,
+ self.style.get_box().transform_style.to_webrender(),
+ wr::PropertyBinding::Value(reference_frame_data.transform),
+ reference_frame_data.kind,
+ );
+
+ // WebRender reference frames establish a new coordinate system at their
+ // origin (the border box of the fragment). We need to ensure that any
+ // coordinates we give to WebRender in this reference frame are relative
+ // to the fragment border box. We do this by adjusting the containing
+ // block origin. Note that the `for_absolute_descendants` and
+ // `for_all_absolute_and_fixed_descendants` properties are now bogus,
+ // but all fragments that establish reference frames also establish
+ // containing blocks for absolute and fixed descendants, so those
+ // properties will be replaced before recursing into children.
+ assert!(
+ self.style
+ .establishes_containing_block_for_all_descendants(self.base.flags)
+ );
+ let adjusted_containing_block = ContainingBlock::new(
+ containing_block
+ .rect
+ .translate(-reference_frame_data.origin.to_vector()),
+ new_spatial_id,
+ None,
+ containing_block.clip_chain_id,
+ );
+ let new_containing_block_info =
+ containing_block_info.new_for_non_absolute_descendants(&adjusted_containing_block);
+
+ self.build_stacking_context_tree_maybe_creating_stacking_context(
+ fragment,
+ display_list,
+ &adjusted_containing_block,
+ &new_containing_block_info,
+ parent_stacking_context,
+ );
+
+ display_list.pop_reference_frame();
+ }
+
+ fn build_stacking_context_tree_maybe_creating_stacking_context(
+ &self,
+ fragment: Fragment,
+ display_list: &mut DisplayList,
+ containing_block: &ContainingBlock,
+ containing_block_info: &ContainingBlockInfo,
+ parent_stacking_context: &mut StackingContext,
+ ) {
+ let context_type = match self.get_stacking_context_type() {
+ Some(context_type) => context_type,
+ None => {
+ self.build_stacking_context_tree_for_children(
+ fragment,
+ display_list,
+ containing_block,
+ containing_block_info,
+ parent_stacking_context,
+ );
+ return;
+ },
+ };
+
+ if context_type == StackingContextType::AtomicInlineStackingContainer {
+ // Push a dummy fragment that indicates when the new stacking context should be painted.
+ parent_stacking_context.contents.push(
+ StackingContextContent::AtomicInlineStackingContainer {
+ index: parent_stacking_context
+ .atomic_inline_stacking_containers
+ .len(),
+ },
+ );
+ }
+
+ // `clip-path` needs to be applied before filters and creates a stacking context, so it can be
+ // applied directly to the stacking context itself.
+ // before
+ let stacking_context_clip_chain_id = build_clip_path_clip_chain_if_necessary(
+ self.style.clone_clip_path(),
+ display_list,
+ &containing_block.scroll_node_id,
+ &containing_block.clip_chain_id,
+ BuilderForBoxFragment::new(
+ self,
+ &containing_block.rect,
+ false, /* is_hit_test_for_scrollable_overflow */
+ false, /* is_collapsed_table_borders */
+ ),
+ )
+ .unwrap_or(containing_block.clip_chain_id);
+
+ let mut child_stacking_context = parent_stacking_context.create_descendant(
+ containing_block.scroll_node_id.spatial_id,
+ stacking_context_clip_chain_id,
+ self.style.clone(),
+ self.base.flags,
+ context_type,
+ );
+ self.build_stacking_context_tree_for_children(
+ fragment,
+ display_list,
+ containing_block,
+ containing_block_info,
+ &mut child_stacking_context,
+ );
+
+ let mut stolen_children = vec![];
+ if context_type != StackingContextType::RealStackingContext {
+ stolen_children = mem::replace(
+ &mut child_stacking_context
+ .real_stacking_contexts_and_positioned_stacking_containers,
+ stolen_children,
+ );
+ }
+
+ child_stacking_context.sort();
+ parent_stacking_context.add_stacking_context(child_stacking_context);
+ parent_stacking_context
+ .real_stacking_contexts_and_positioned_stacking_containers
+ .append(&mut stolen_children);
+ }
+
+ fn build_stacking_context_tree_for_children(
+ &self,
+ fragment: Fragment,
+ display_list: &mut DisplayList,
+ containing_block: &ContainingBlock,
+ containing_block_info: &ContainingBlockInfo,
+ stacking_context: &mut StackingContext,
+ ) {
+ let mut new_scroll_node_id = containing_block.scroll_node_id;
+ let mut new_clip_chain_id = containing_block.clip_chain_id;
+ let mut new_scroll_frame_size = containing_block_info
+ .for_non_absolute_descendants
+ .scroll_frame_size;
+
+ if let Some(scroll_node_id) = self.build_sticky_frame_if_necessary(
+ display_list,
+ &new_scroll_node_id,
+ &containing_block.rect,
+ &new_scroll_frame_size,
+ ) {
+ new_scroll_node_id = scroll_node_id;
+ }
+
+ if let Some(clip_chain_id) = self.build_clip_frame_if_necessary(
+ display_list,
+ &new_scroll_node_id,
+ &new_clip_chain_id,
+ &containing_block.rect,
+ ) {
+ new_clip_chain_id = clip_chain_id;
+ }
+
+ if let Some(clip_chain_id) = build_clip_path_clip_chain_if_necessary(
+ self.style.clone_clip_path(),
+ display_list,
+ &new_scroll_node_id,
+ &new_clip_chain_id,
+ BuilderForBoxFragment::new(
+ self,
+ &containing_block.rect,
+ false, /* is_hit_test_for_scrollable_overflow*/
+ false, /* is_collapsed_table_borders */
+ ),
+ ) {
+ new_clip_chain_id = clip_chain_id;
+ }
+
+ let establishes_containing_block_for_all_descendants = self
+ .style
+ .establishes_containing_block_for_all_descendants(self.base.flags);
+ let establishes_containing_block_for_absolute_descendants = self
+ .style
+ .establishes_containing_block_for_absolute_descendants(self.base.flags);
+
+ let reference_frame_scroll_node_id_for_fragments =
+ if establishes_containing_block_for_all_descendants {
+ new_scroll_node_id
+ } else {
+ containing_block_info
+ .for_absolute_and_fixed_descendants
+ .scroll_node_id
+ };
+
+ let mut add_fragment = |section| {
+ stacking_context
+ .contents
+ .push(StackingContextContent::Fragment {
+ scroll_node_id: new_scroll_node_id,
+ reference_frame_scroll_node_id: reference_frame_scroll_node_id_for_fragments,
+ clip_chain_id: new_clip_chain_id,
+ section,
+ containing_block: containing_block.rect,
+ fragment: fragment.clone(),
+ is_hit_test_for_scrollable_overflow: false,
+ is_collapsed_table_borders: false,
+ });
+ };
+
+ let section = self.get_stacking_context_section();
+ add_fragment(section);
+ if !self.style.get_outline().outline_width.is_zero() {
+ add_fragment(StackingContextSection::Outline);
+ }
+
+ // We want to build the scroll frame after the background and border, because
+ // they shouldn't scroll with the rest of the box content.
+ if let Some(overflow_frame_data) = self.build_overflow_frame_if_necessary(
+ display_list,
+ &new_scroll_node_id,
+ &new_clip_chain_id,
+ &containing_block.rect,
+ ) {
+ new_clip_chain_id = overflow_frame_data.clip_chain_id;
+ if let Some(scroll_frame_data) = overflow_frame_data.scroll_frame_data {
+ new_scroll_node_id = scroll_frame_data.scroll_tree_node_id;
+ new_scroll_frame_size = Some(scroll_frame_data.scroll_frame_rect.size());
+
+ stacking_context
+ .contents
+ .push(StackingContextContent::Fragment {
+ scroll_node_id: new_scroll_node_id,
+ reference_frame_scroll_node_id:
+ reference_frame_scroll_node_id_for_fragments,
+ clip_chain_id: new_clip_chain_id,
+ section,
+ containing_block: containing_block.rect,
+ fragment: fragment.clone(),
+ is_hit_test_for_scrollable_overflow: true,
+ is_collapsed_table_borders: false,
+ });
+ }
+ }
+
+ let padding_rect = self
+ .padding_rect()
+ .translate(containing_block.rect.origin.to_vector());
+ let content_rect = self
+ .content_rect
+ .translate(containing_block.rect.origin.to_vector());
+
+ let for_absolute_descendants = ContainingBlock::new(
+ padding_rect,
+ new_scroll_node_id,
+ new_scroll_frame_size,
+ new_clip_chain_id,
+ );
+ let for_non_absolute_descendants = ContainingBlock::new(
+ content_rect,
+ new_scroll_node_id,
+ new_scroll_frame_size,
+ new_clip_chain_id,
+ );
+
+ // Create a new `ContainingBlockInfo` for descendants depending on
+ // whether or not this fragment establishes a containing block for
+ // absolute and fixed descendants.
+ let new_containing_block_info = if establishes_containing_block_for_all_descendants {
+ containing_block_info.new_for_absolute_and_fixed_descendants(
+ &for_non_absolute_descendants,
+ &for_absolute_descendants,
+ )
+ } else if establishes_containing_block_for_absolute_descendants {
+ containing_block_info.new_for_absolute_descendants(
+ &for_non_absolute_descendants,
+ &for_absolute_descendants,
+ )
+ } else {
+ containing_block_info.new_for_non_absolute_descendants(&for_non_absolute_descendants)
+ };
+
+ for child in &self.children {
+ child.build_stacking_context_tree(
+ display_list,
+ &new_containing_block_info,
+ stacking_context,
+ StackingContextBuildMode::SkipHoisted,
+ );
+ }
+
+ if matches!(&fragment, Fragment::Box(box_fragment) if matches!(
+ box_fragment.borrow().specific_layout_info,
+ Some(SpecificLayoutInfo::TableGridWithCollapsedBorders(_))
+ )) {
+ stacking_context
+ .contents
+ .push(StackingContextContent::Fragment {
+ scroll_node_id: new_scroll_node_id,
+ reference_frame_scroll_node_id: reference_frame_scroll_node_id_for_fragments,
+ clip_chain_id: new_clip_chain_id,
+ section,
+ containing_block: containing_block.rect,
+ fragment: fragment.clone(),
+ is_hit_test_for_scrollable_overflow: false,
+ is_collapsed_table_borders: true,
+ });
+ }
+ }
+
+ fn build_clip_frame_if_necessary(
+ &self,
+ display_list: &mut DisplayList,
+ parent_scroll_node_id: &ScrollTreeNodeId,
+ parent_clip_chain_id: &wr::ClipChainId,
+ containing_block_rect: &PhysicalRect<Au>,
+ ) -> Option<wr::ClipChainId> {
+ let position = self.style.get_box().position;
+ // https://drafts.csswg.org/css2/#clipping
+ // The clip property applies only to absolutely positioned elements
+ if position != ComputedPosition::Absolute && position != ComputedPosition::Fixed {
+ return None;
+ }
+
+ // Only rectangles are supported for now.
+ let clip_rect = match self.style.get_effects().clip {
+ ClipRectOrAuto::Rect(rect) => rect,
+ _ => return None,
+ };
+
+ let border_rect = self.border_rect();
+ let clip_rect = clip_rect
+ .for_border_rect(border_rect)
+ .translate(containing_block_rect.origin.to_vector())
+ .to_webrender();
+
+ let clip_id = display_list
+ .wr
+ .define_clip_rect(parent_scroll_node_id.spatial_id, clip_rect);
+ Some(display_list.define_clip_chain(*parent_clip_chain_id, [clip_id]))
+ }
+
+ fn build_overflow_frame_if_necessary(
+ &self,
+ display_list: &mut DisplayList,
+ parent_scroll_node_id: &ScrollTreeNodeId,
+ parent_clip_chain_id: &wr::ClipChainId,
+ containing_block_rect: &PhysicalRect<Au>,
+ ) -> Option<OverflowFrameData> {
+ let overflow = self.style.effective_overflow(self.base.flags);
+
+ if overflow.x == ComputedOverflow::Visible && overflow.y == ComputedOverflow::Visible {
+ return None;
+ }
+
+ // Non-scrollable overflow path
+ if overflow.x == ComputedOverflow::Clip || overflow.y == ComputedOverflow::Clip {
+ // TODO: The spec allows `overflow-clip-rect` to specify which box edge to use
+ // as the overflow clip edge origin, but Stylo doesn't currently support that.
+ // It will need to be handled here, for now always use the padding rect.
+ let mut overflow_clip_rect = self
+ .padding_rect()
+ .translate(containing_block_rect.origin.to_vector())
+ .to_webrender();
+
+ // Adjust by the overflow clip margin.
+ // https://drafts.csswg.org/css-overflow-3/#overflow-clip-margin
+ let clip_margin = self.style.get_margin().overflow_clip_margin.px();
+ overflow_clip_rect = overflow_clip_rect.inflate(clip_margin, clip_margin);
+
+ // The clipping region only gets rounded corners if both axes have `overflow: clip`.
+ // https://drafts.csswg.org/css-overflow-3/#corner-clipping
+ let radii;
+ if overflow.x == ComputedOverflow::Clip && overflow.y == ComputedOverflow::Clip {
+ let builder = BuilderForBoxFragment::new(self, containing_block_rect, false, false);
+ radii = offset_radii(builder.border_radius, clip_margin);
+ } else if overflow.x != ComputedOverflow::Clip {
+ overflow_clip_rect.min.x = f32::MIN;
+ overflow_clip_rect.max.x = f32::MAX;
+ radii = BorderRadius::zero();
+ } else {
+ overflow_clip_rect.min.y = f32::MIN;
+ overflow_clip_rect.max.y = f32::MAX;
+ radii = BorderRadius::zero();
+ }
+
+ let clip_chain_id = display_list.clip_overflow_frame(
+ parent_scroll_node_id,
+ parent_clip_chain_id,
+ overflow_clip_rect,
+ radii,
+ );
+
+ return Some(OverflowFrameData {
+ clip_chain_id,
+ scroll_frame_data: None,
+ });
+ }
+
+ // scrollable overflow path
+ // From https://drafts.csswg.org/css-overflow/#propdef-overflow:
+ // > UAs must apply the overflow-* values set on the root element to the viewport when the
+ // > root element’s display value is not none. However, when the root element is an [HTML]
+ // > html element (including XML syntax for HTML) whose overflow value is visible (in both
+ // > axes), and that element has as a child a body element whose display value is also not
+ // > none, user agents must instead apply the overflow-* values of the first such child
+ // > element to the viewport. The element from which the value is propagated must then have a
+ // > used overflow value of visible.
+ //
+ // TODO: This should only happen when the `display` value is actually propagated.
+ if self
+ .base
+ .flags
+ .contains(FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT)
+ {
+ return None;
+ }
+
+ let scroll_frame_rect = self
+ .padding_rect()
+ .translate(containing_block_rect.origin.to_vector())
+ .to_webrender();
+
+ let clip_chain_id = display_list.clip_overflow_frame(
+ parent_scroll_node_id,
+ parent_clip_chain_id,
+ scroll_frame_rect,
+ BuilderForBoxFragment::new(self, containing_block_rect, false, false).border_radius,
+ );
+
+ let tag = self.base.tag?;
+ let external_id = wr::ExternalScrollId(
+ tag.to_display_list_fragment_id(),
+ display_list.wr.pipeline_id,
+ );
+
+ let sensitivity = AxesScrollSensitivity {
+ x: overflow.x.into(),
+ y: overflow.y.into(),
+ };
+
+ let content_rect = self.scrollable_overflow().to_webrender();
+
+ let scroll_tree_node_id = display_list.define_scroll_frame(
+ parent_scroll_node_id,
+ external_id,
+ content_rect,
+ scroll_frame_rect,
+ sensitivity,
+ );
+
+ Some(OverflowFrameData {
+ clip_chain_id,
+ scroll_frame_data: Some(ScrollFrameData {
+ scroll_tree_node_id,
+ scroll_frame_rect,
+ }),
+ })
+ }
+
+ fn build_sticky_frame_if_necessary(
+ &self,
+ display_list: &mut DisplayList,
+ parent_scroll_node_id: &ScrollTreeNodeId,
+ containing_block_rect: &PhysicalRect<Au>,
+ scroll_frame_size: &Option<LayoutSize>,
+ ) -> Option<ScrollTreeNodeId> {
+ if self.style.get_box().position != ComputedPosition::Sticky {
+ return None;
+ }
+
+ let scroll_frame_size_for_resolve = match scroll_frame_size {
+ Some(size) => size,
+ None => {
+ // This is a direct descendant of a reference frame.
+ &display_list.compositor_info.viewport_size
+ },
+ };
+
+ // Percentages sticky positions offsets are resovled against the size of the
+ // nearest scroll frame instead of the containing block like for other types
+ // of positioning.
+ let scroll_frame_height = Au::from_f32_px(scroll_frame_size_for_resolve.height);
+ let scroll_frame_width = Au::from_f32_px(scroll_frame_size_for_resolve.width);
+ let offsets = self.style.physical_box_offsets();
+ let offsets = PhysicalSides::<AuOrAuto>::new(
+ offsets.top.map(|v| v.to_used_value(scroll_frame_height)),
+ offsets.right.map(|v| v.to_used_value(scroll_frame_width)),
+ offsets.bottom.map(|v| v.to_used_value(scroll_frame_height)),
+ offsets.left.map(|v| v.to_used_value(scroll_frame_width)),
+ );
+ *self.resolved_sticky_insets.borrow_mut() = Some(offsets);
+
+ if scroll_frame_size.is_none() {
+ return None;
+ }
+
+ if offsets.top.is_auto() &&
+ offsets.right.is_auto() &&
+ offsets.bottom.is_auto() &&
+ offsets.left.is_auto()
+ {
+ return None;
+ }
+
+ let frame_rect = self
+ .border_rect()
+ .translate(containing_block_rect.origin.to_vector())
+ .to_webrender();
+
+ // Position:sticky elements are always restricted based on the size and position of their
+ // containing block.
+ let containing_block_rect = containing_block_rect.to_webrender();
+
+ // This is the minimum negative offset and then the maximum positive offset. We just
+ // specify every edge, but if the corresponding margin is None, that offset has no effect.
+ let vertical_offset_bounds = wr::StickyOffsetBounds::new(
+ containing_block_rect.min.y - frame_rect.min.y,
+ containing_block_rect.max.y - frame_rect.max.y,
+ );
+ let horizontal_offset_bounds = wr::StickyOffsetBounds::new(
+ containing_block_rect.min.x - frame_rect.min.x,
+ containing_block_rect.max.x - frame_rect.max.x,
+ );
+
+ let margins = SideOffsets2D::new(
+ offsets.top.non_auto().map(|v| v.to_f32_px()),
+ offsets.right.non_auto().map(|v| v.to_f32_px()),
+ offsets.bottom.non_auto().map(|v| v.to_f32_px()),
+ offsets.left.non_auto().map(|v| v.to_f32_px()),
+ );
+
+ let sticky_node_id = display_list.define_sticky_frame(
+ parent_scroll_node_id,
+ frame_rect,
+ margins,
+ vertical_offset_bounds,
+ horizontal_offset_bounds,
+ );
+
+ Some(sticky_node_id)
+ }
+
+ /// Optionally returns the data for building a reference frame, without yet building it.
+ fn reference_frame_data_if_necessary(
+ &self,
+ containing_block_rect: &PhysicalRect<Au>,
+ ) -> Option<ReferenceFrameData> {
+ if !self.style.has_transform_or_perspective(self.base.flags) {
+ return None;
+ }
+
+ let relative_border_rect = self.border_rect();
+ let border_rect = relative_border_rect.translate(containing_block_rect.origin.to_vector());
+ let untyped_border_rect = border_rect.to_untyped();
+
+ let transform = self.calculate_transform_matrix(&untyped_border_rect);
+ let perspective = self.calculate_perspective_matrix(&untyped_border_rect);
+ let (reference_frame_transform, reference_frame_kind) = match (transform, perspective) {
+ (None, Some(perspective)) => (
+ perspective,
+ wr::ReferenceFrameKind::Perspective {
+ scrolling_relative_to: None,
+ },
+ ),
+ (Some(transform), None) => (
+ transform,
+ wr::ReferenceFrameKind::Transform {
+ is_2d_scale_translation: false,
+ should_snap: false,
+ paired_with_perspective: false,
+ },
+ ),
+ (Some(transform), Some(perspective)) => (
+ perspective.then(&transform),
+ wr::ReferenceFrameKind::Perspective {
+ scrolling_relative_to: None,
+ },
+ ),
+ (None, None) => unreachable!(),
+ };
+
+ Some(ReferenceFrameData {
+ origin: border_rect.origin,
+ transform: reference_frame_transform,
+ kind: reference_frame_kind,
+ })
+ }
+
+ /// Returns true if the given style contains a transform that is not invertible.
+ fn has_non_invertible_transform_or_zero_scale(&self, containing_block: &Rect<Au>) -> bool {
+ let list = &self.style.get_box().transform;
+ match list.to_transform_3d_matrix(Some(&au_rect_to_length_rect(containing_block))) {
+ Ok(t) => !t.0.is_invertible() || t.0.m11 == 0. || t.0.m22 == 0.,
+ Err(_) => false,
+ }
+ }
+
+ /// Returns the 4D matrix representing this fragment's transform.
+ pub fn calculate_transform_matrix(&self, border_rect: &Rect<Au>) -> Option<LayoutTransform> {
+ let list = &self.style.get_box().transform;
+ let length_rect = au_rect_to_length_rect(border_rect);
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ let rotate = match self.style.clone_rotate() {
+ GenericRotate::Rotate(angle) => (0., 0., 1., angle),
+ GenericRotate::Rotate3D(x, y, z, angle) => (x, y, z, angle),
+ GenericRotate::None => (0., 0., 1., Angle::zero()),
+ };
+ let scale = match self.style.clone_scale() {
+ GenericScale::Scale(sx, sy, sz) => (sx, sy, sz),
+ GenericScale::None => (1., 1., 1.),
+ };
+ let translation = match self.style.clone_translate() {
+ GenericTranslate::Translate(x, y, z) => LayoutTransform::translation(
+ x.resolve(length_rect.size.width).px(),
+ y.resolve(length_rect.size.height).px(),
+ z.px(),
+ ),
+ GenericTranslate::None => LayoutTransform::identity(),
+ };
+
+ let angle = euclid::Angle::radians(rotate.3.radians());
+ let transform_base = list.to_transform_3d_matrix(Some(&length_rect)).ok()?;
+ let transform = LayoutTransform::from_untyped(&transform_base.0)
+ .then_rotate(rotate.0, rotate.1, rotate.2, angle)
+ .then_scale(scale.0, scale.1, scale.2)
+ .then(&translation);
+ // WebRender will end up dividing by the scale value of this transform, so we
+ // want to ensure we don't feed it a divisor of 0.
+ if transform.m11 == 0. || transform.m22 == 0. {
+ return Some(LayoutTransform::identity());
+ }
+
+ let transform_origin = &self.style.get_box().transform_origin;
+ let transform_origin_x = transform_origin
+ .horizontal
+ .to_used_value(border_rect.size.width)
+ .to_f32_px();
+ let transform_origin_y = transform_origin
+ .vertical
+ .to_used_value(border_rect.size.height)
+ .to_f32_px();
+ let transform_origin_z = transform_origin.depth.px();
+
+ Some(transform.change_basis(transform_origin_x, transform_origin_y, transform_origin_z))
+ }
+
+ /// Returns the 4D matrix representing this fragment's perspective.
+ pub fn calculate_perspective_matrix(&self, border_rect: &Rect<Au>) -> Option<LayoutTransform> {
+ match self.style.get_box().perspective {
+ Perspective::Length(length) => {
+ let perspective_origin = &self.style.get_box().perspective_origin;
+ let perspective_origin = LayoutPoint::new(
+ perspective_origin
+ .horizontal
+ .percentage_relative_to(border_rect.size.width.into())
+ .px(),
+ perspective_origin
+ .vertical
+ .percentage_relative_to(border_rect.size.height.into())
+ .px(),
+ );
+
+ let perspective_matrix = LayoutTransform::from_untyped(
+ &transform::create_perspective_matrix(length.px()),
+ );
+
+ Some(perspective_matrix.change_basis(
+ perspective_origin.x,
+ perspective_origin.y,
+ 0.0,
+ ))
+ },
+ Perspective::None => None,
+ }
+ }
+}
+
+impl PositioningFragment {
+ fn build_stacking_context_tree(
+ &self,
+ display_list: &mut DisplayList,
+ containing_block: &ContainingBlock,
+ containing_block_info: &ContainingBlockInfo,
+ stacking_context: &mut StackingContext,
+ ) {
+ let rect = self
+ .rect
+ .translate(containing_block.rect.origin.to_vector());
+ let new_containing_block = containing_block.new_replacing_rect(&rect);
+ let new_containing_block_info =
+ containing_block_info.new_for_non_absolute_descendants(&new_containing_block);
+
+ for child in &self.children {
+ child.build_stacking_context_tree(
+ display_list,
+ &new_containing_block_info,
+ stacking_context,
+ StackingContextBuildMode::SkipHoisted,
+ );
+ }
+ }
+}
+
+pub fn au_rect_to_length_rect(rect: &Rect<Au>) -> Rect<Length> {
+ Rect::new(
+ Point2D::new(rect.origin.x.into(), rect.origin.y.into()),
+ Size2D::new(rect.size.width.into(), rect.size.height.into()),
+ )
+}