diff --git a/components/layout_2020/display_list/background.rs b/components/layout_2020/display_list/background.rs new file mode 100644 index 00000000000..492ef38922f --- /dev/null +++ b/components/layout_2020/display_list/background.rs @@ -0,0 +1,238 @@ +/* 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 crate::replaced::IntrinsicSizes; +use euclid::{Size2D, Vector2D}; +use style::computed_values::background_clip::single_value::T as Clip; +use style::computed_values::background_origin::single_value::T as Origin; +use style::values::computed::background::BackgroundSize as Size; +use style::values::computed::{Length, LengthPercentage}; +use style::values::specified::background::BackgroundRepeat as RepeatXY; +use style::values::specified::background::BackgroundRepeatKeyword as Repeat; +use webrender_api::{self as wr, units}; + +pub(super) struct Layer { + pub common: wr::CommonItemProperties, + pub bounds: units::LayoutRect, + pub tile_size: units::LayoutSize, + pub tile_spacing: units::LayoutSize, + pub repeat: bool, +} + +struct Layout1DResult { + repeat: bool, + bounds_origin: f32, + bounds_size: f32, +} + +fn get_cyclic(values: &[T], layer_index: usize) -> &T { + &values[layer_index % values.len()] +} + +pub(super) fn layout_layer( + fragment_builder: &mut super::BuilderForBoxFragment, + builder: &mut super::DisplayListBuilder, + layer_index: usize, + intrinsic: IntrinsicSizes, +) -> Option { + let b = fragment_builder.fragment.style.get_background(); + + let clipping_area = match get_cyclic(&b.background_clip.0, layer_index) { + Clip::ContentBox => fragment_builder.content_rect(), + Clip::PaddingBox => fragment_builder.padding_rect(), + Clip::BorderBox => &fragment_builder.border_rect, + }; + let positioning_area = match get_cyclic(&b.background_origin.0, layer_index) { + Origin::ContentBox => fragment_builder.content_rect(), + Origin::PaddingBox => fragment_builder.padding_rect(), + Origin::BorderBox => &fragment_builder.border_rect, + }; + + // 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(intrinsic_ratio) = intrinsic.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 <= intrinsic_ratio, + ContainOrCover::Cover => positioning_ratio > intrinsic_ratio, + }; + // The other dimension needs to be adjusted + if fit_width { + tile_size.height = tile_size.width / intrinsic_ratio + } else { + tile_size.width = tile_size.height * intrinsic_ratio + } + } + tile_size + }; + 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.percentage_relative_to(Length::new(positioning_area.size.width)) + }); + let mut height = height.non_auto().map(|lp| { + lp.0.percentage_relative_to(Length::new(positioning_area.size.height)) + }); + + if width.is_none() && height.is_none() { + // Both computed values are 'auto': + // use intrinsic sizes, treating missing width or height as 'auto' + width = intrinsic.width; + height = intrinsic.height; + } + + match (width, height) { + (Some(w), Some(h)) => units::LayoutSize::new(w.px(), h.px()), + (Some(w), None) => { + let h = if let Some(intrinsic_ratio) = intrinsic.ratio { + w / intrinsic_ratio + } else if let Some(intrinsic_height) = intrinsic.height { + intrinsic_height + } else { + // Treated as 100% + Length::new(positioning_area.size.height) + }; + units::LayoutSize::new(w.px(), h.px()) + }, + (None, Some(h)) => { + let w = if let Some(intrinsic_ratio) = intrinsic.ratio { + h * intrinsic_ratio + } else if let Some(intrinsic_width) = intrinsic.width { + intrinsic_width + } else { + // Treated as 100% + Length::new(positioning_area.size.width) + }; + units::LayoutSize::new(w.px(), h.px()) + }, + // Both comptued values were 'auto', and neither intrinsic 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 mut tile_spacing = units::LayoutSize::zero(); + let RepeatXY(repeat_x, repeat_y) = *get_cyclic(&b.background_repeat.0, layer_index); + let result_x = layout_1d( + &mut tile_size.width, + &mut tile_spacing.width, + repeat_x, + get_cyclic(&b.background_position_x.0, layer_index), + clipping_area.origin.x - positioning_area.origin.x, + clipping_area.size.width, + positioning_area.size.width, + ); + let result_y = layout_1d( + &mut tile_size.height, + &mut tile_spacing.height, + repeat_y, + get_cyclic(&b.background_position_y.0, layer_index), + clipping_area.origin.y - positioning_area.origin.y, + clipping_area.size.height, + positioning_area.size.height, + ); + let bounds = units::LayoutRect::new( + positioning_area.origin + Vector2D::new(result_x.bounds_origin, result_y.bounds_origin), + Size2D::new(result_x.bounds_size, result_y.bounds_size), + ); + + // The 'backgound-clip' property maps directly to `clip_rect` in `CommonItemProperties`: + let mut common = builder.common_properties(*clipping_area); + fragment_builder.with_border_edge_clip(builder, &mut common); + + Some(Layer { + 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, + tile_spacing: &mut f32, + mut repeat: Repeat, + position: &LengthPercentage, + clipping_area_origin: f32, + clipping_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 + .percentage_relative_to(Length::new(positioning_area_size - *tile_size)) + .px(); + // 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 - clipping_area_origin; + let bounds_origin = position - tile_stride * (offset / tile_stride).ceil(); + let bounds_size = clipping_area_size - bounds_origin - clipping_area_origin; + Layout1DResult { + repeat: true, + bounds_origin, + bounds_size, + } + }, + 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, + } + }, + } +} diff --git a/components/layout_2020/display_list/mod.rs b/components/layout_2020/display_list/mod.rs index fb6b9b54a69..9d4deaa265b 100644 --- a/components/layout_2020/display_list/mod.rs +++ b/components/layout_2020/display_list/mod.rs @@ -7,7 +7,7 @@ use crate::fragments::{BoxFragment, Fragment}; use crate::geom::{PhysicalPoint, PhysicalRect, ToWebRender}; use crate::replaced::IntrinsicSizes; use embedder_traits::Cursor; -use euclid::{Point2D, SideOffsets2D, Size2D, Vector2D}; +use euclid::{Point2D, SideOffsets2D, Size2D}; use gfx::text::glyph::GlyphStore; use mitochondria::OnceCell; use net_traits::image_cache::UsePlaceholder; @@ -18,6 +18,8 @@ use style::values::computed::{BorderStyle, Length, LengthPercentage}; use style::values::specified::ui::CursorKind; use webrender_api::{self as wr, units}; +mod background; + #[derive(Clone, Copy)] pub struct WebRenderImageInfo { pub width: u32, @@ -264,29 +266,62 @@ impl<'a> BuilderForBoxFragment<'a> { // TODO }, Image::Url(image_url) => { - if let Some(url) = image_url.url() { - let webrender_image = builder.context.get_webrender_image_for_url( - self.fragment.tag, - url.clone(), - UsePlaceholder::No, - ); - if let Some(WebRenderImageInfo { - width, - height, - key: Some(key), - }) = webrender_image - { - // FIXME: https://drafts.csswg.org/css-images-4/#the-image-resolution - let dppx = 1.0; + // FIXME: images won’t always have in intrinsic width or height + // when support for SVG is added. + // Or a WebRender `ImageKey`, for that matter. + let (width, height, key) = match image_url.url() { + Some(url) => { + match builder.context.get_webrender_image_for_url( + self.fragment.tag, + url.clone(), + UsePlaceholder::No, + ) { + Some(WebRenderImageInfo { + width, + height, + key: Some(key), + }) => (width, height, key), + _ => continue, + } + }, + None => continue, + }; - let intrinsic = IntrinsicSizes { - width: Some(Length::new(width as f32 / dppx)), - height: Some(Length::new(height as f32 / dppx)), - // FIXME https://github.com/w3c/csswg-drafts/issues/4572 - ratio: Some(width as f32 / height as f32), - }; + // FIXME: https://drafts.csswg.org/css-images-4/#the-image-resolution + let dppx = 1.0; - self.build_background_raster_image(builder, index, intrinsic, key) + let intrinsic = IntrinsicSizes { + width: Some(Length::new(width as f32 / dppx)), + height: Some(Length::new(height as f32 / dppx)), + // FIXME https://github.com/w3c/csswg-drafts/issues/4572 + ratio: Some(width as f32 / height as f32), + }; + + if let Some(layer) = + background::layout_layer(self, builder, index, intrinsic) + { + let image_rendering = + image_rendering(self.fragment.style.clone_image_rendering()); + if layer.repeat { + builder.wr.push_repeating_image( + &layer.common, + layer.bounds, + layer.tile_size, + layer.tile_spacing, + image_rendering, + wr::AlphaType::PremultipliedAlpha, + key, + wr::ColorF::WHITE, + ) + } else { + builder.wr.push_image( + &layer.common, + layer.bounds, + image_rendering, + wr::AlphaType::PremultipliedAlpha, + key, + wr::ColorF::WHITE, + ) } } }, @@ -297,244 +332,6 @@ impl<'a> BuilderForBoxFragment<'a> { } } - fn build_background_raster_image( - &mut self, - builder: &mut DisplayListBuilder, - index: usize, - intrinsic: IntrinsicSizes, - key: wr::ImageKey, - ) { - use style::computed_values::background_clip::single_value::T as Clip; - use style::computed_values::background_origin::single_value::T as Origin; - use style::values::computed::background::BackgroundSize as Size; - use style::values::specified::background::BackgroundRepeat as RepeatXY; - use style::values::specified::background::BackgroundRepeatKeyword as Repeat; - - fn get_cyclic(values: &[T], index: usize) -> &T { - &values[index % values.len()] - } - - let b = self.fragment.style.get_background(); - - let clipping_area = match get_cyclic(&b.background_clip.0, index) { - Clip::ContentBox => self.content_rect(), - Clip::PaddingBox => self.padding_rect(), - Clip::BorderBox => &self.border_rect, - }; - let positioning_area = match get_cyclic(&b.background_origin.0, index) { - Origin::ContentBox => self.content_rect(), - Origin::PaddingBox => self.padding_rect(), - Origin::BorderBox => &self.border_rect, - }; - - // 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(intrinsic_ratio) = intrinsic.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 <= intrinsic_ratio, - ContainOrCover::Cover => positioning_ratio > intrinsic_ratio, - }; - // The other dimension needs to be adjusted - if fit_width { - tile_size.height = tile_size.width / intrinsic_ratio - } else { - tile_size.width = tile_size.height * intrinsic_ratio - } - } - tile_size - }; - let mut tile_size = match get_cyclic(&b.background_size.0, 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.percentage_relative_to(Length::new(positioning_area.size.width)) - }); - let mut height = height.non_auto().map(|lp| { - lp.0.percentage_relative_to(Length::new(positioning_area.size.height)) - }); - - if width.is_none() && height.is_none() { - // Both computed values are 'auto': - // use intrinsic sizes, treating missing width or height as 'auto' - width = intrinsic.width; - height = intrinsic.height; - } - - match (width, height) { - (Some(w), Some(h)) => units::LayoutSize::new(w.px(), h.px()), - (Some(w), None) => { - let h = if let Some(intrinsic_ratio) = intrinsic.ratio { - w / intrinsic_ratio - } else if let Some(intrinsic_height) = intrinsic.height { - intrinsic_height - } else { - // Treated as 100% - Length::new(positioning_area.size.height) - }; - units::LayoutSize::new(w.px(), h.px()) - }, - (None, Some(h)) => { - let w = if let Some(intrinsic_ratio) = intrinsic.ratio { - h * intrinsic_ratio - } else if let Some(intrinsic_width) = intrinsic.width { - intrinsic_width - } else { - // Treated as 100% - Length::new(positioning_area.size.width) - }; - units::LayoutSize::new(w.px(), h.px()) - }, - // Both comptued values were 'auto', and neither intrinsic size is present - (None, None) => size_contain_or_cover(ContainOrCover::Contain), - } - }, - }; - - if tile_size.width == 0.0 || tile_size.height == 0.0 { - return; - } - - struct Layout1DResult { - repeat: bool, - bounds_origin: f32, - bounds_size: f32, - } - - /// 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, - tile_spacing: &mut f32, - mut repeat: Repeat, - position: &LengthPercentage, - clipping_area_origin: f32, - clipping_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 - .percentage_relative_to(Length::new(positioning_area_size - *tile_size)) - .px(); - // 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 - clipping_area_origin; - let bounds_origin = position - tile_stride * (offset / tile_stride).ceil(); - let bounds_size = clipping_area_size - bounds_origin - clipping_area_origin; - Layout1DResult { - repeat: true, - bounds_origin, - bounds_size, - } - }, - 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, - } - }, - } - } - - let mut tile_spacing = units::LayoutSize::zero(); - let RepeatXY(repeat_x, repeat_y) = *get_cyclic(&b.background_repeat.0, index); - let result_x = layout_1d( - &mut tile_size.width, - &mut tile_spacing.width, - repeat_x, - get_cyclic(&b.background_position_x.0, index), - clipping_area.origin.x - positioning_area.origin.x, - clipping_area.size.width, - positioning_area.size.width, - ); - let result_y = layout_1d( - &mut tile_size.height, - &mut tile_spacing.height, - repeat_y, - get_cyclic(&b.background_position_y.0, index), - clipping_area.origin.y - positioning_area.origin.y, - clipping_area.size.height, - positioning_area.size.height, - ); - let bounds = units::LayoutRect::new( - positioning_area.origin + Vector2D::new(result_x.bounds_origin, result_y.bounds_origin), - Size2D::new(result_x.bounds_size, result_y.bounds_size), - ); - - // The 'backgound-clip' property maps directly to `clip_rect` in `CommonItemProperties`: - let mut common = builder.common_properties(*clipping_area); - self.with_border_edge_clip(builder, &mut common); - - if result_x.repeat || result_y.repeat { - builder.wr.push_repeating_image( - &common, - bounds, - tile_size, - tile_spacing, - image_rendering(self.fragment.style.clone_image_rendering()), - wr::AlphaType::PremultipliedAlpha, - key, - wr::ColorF::WHITE, - ) - } else { - builder.wr.push_image( - &common, - bounds, - image_rendering(self.fragment.style.clone_image_rendering()), - wr::AlphaType::PremultipliedAlpha, - key, - wr::ColorF::WHITE, - ) - } - } - fn build_border(&mut self, builder: &mut DisplayListBuilder) { let b = self.fragment.style.get_border(); let widths = SideOffsets2D::new(