diff options
Diffstat (limited to 'components/layout/display_list')
-rw-r--r-- | components/layout/display_list/background.rs | 361 | ||||
-rw-r--r-- | components/layout/display_list/clip_path.rs | 259 | ||||
-rw-r--r-- | components/layout/display_list/conversions.rs | 160 | ||||
-rw-r--r-- | components/layout/display_list/gradient.rs | 468 | ||||
-rw-r--r-- | components/layout/display_list/mod.rs | 1511 | ||||
-rw-r--r-- | components/layout/display_list/stacking_context.rs | 1748 |
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, ¤t_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, ¤t_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()), + ) +} |