aboutsummaryrefslogtreecommitdiffstats
path: root/components/layout/replaced.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/layout/replaced.rs')
-rw-r--r--components/layout/replaced.rs623
1 files changed, 623 insertions, 0 deletions
diff --git a/components/layout/replaced.rs b/components/layout/replaced.rs
new file mode 100644
index 00000000000..6a6b1979ff9
--- /dev/null
+++ b/components/layout/replaced.rs
@@ -0,0 +1,623 @@
+/* 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::LazyCell;
+use std::fmt;
+use std::sync::Arc;
+
+use app_units::Au;
+use base::id::{BrowsingContextId, PipelineId};
+use data_url::DataUrl;
+use embedder_traits::ViewportDetails;
+use euclid::{Scale, Size2D};
+use malloc_size_of_derive::MallocSizeOf;
+use net_traits::image_cache::{ImageOrMetadataAvailable, UsePlaceholder};
+use pixels::Image;
+use script_layout_interface::IFrameSize;
+use servo_arc::Arc as ServoArc;
+use style::Zero;
+use style::computed_values::object_fit::T as ObjectFit;
+use style::logical_geometry::{Direction, WritingMode};
+use style::properties::ComputedValues;
+use style::servo::url::ComputedUrl;
+use style::values::CSSFloat;
+use style::values::computed::image::Image as ComputedImage;
+use url::Url;
+use webrender_api::ImageKey;
+
+use crate::cell::ArcRefCell;
+use crate::context::LayoutContext;
+use crate::dom::NodeExt;
+use crate::fragment_tree::{BaseFragmentInfo, Fragment, IFrameFragment, ImageFragment};
+use crate::geom::{
+ LogicalSides1D, LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize, Size, Sizes,
+};
+use crate::layout_box_base::LayoutBoxBase;
+use crate::sizing::{ComputeInlineContentSizes, ContentSizes, InlineContentSizesResult};
+use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, ContentBoxSizesAndPBM, LayoutStyle};
+use crate::{ConstraintSpace, ContainingBlock, SizeConstraint};
+
+#[derive(Debug, MallocSizeOf)]
+pub(crate) struct ReplacedContents {
+ pub kind: ReplacedContentKind,
+ natural_size: NaturalSizes,
+ base_fragment_info: BaseFragmentInfo,
+}
+
+/// The natural dimensions of a replaced element, including a height, width, and
+/// aspect ratio.
+///
+/// * Raster images always have an natural width and height, with 1 image pixel = 1px.
+/// The natural ratio should be based on dividing those.
+/// See <https://github.com/w3c/csswg-drafts/issues/4572> for the case where either is zero.
+/// PNG specifically disallows this but I (SimonSapin) am not sure about other formats.
+///
+/// * Form controls have both natural width and height **but no natural ratio**.
+/// See <https://github.com/w3c/csswg-drafts/issues/1044> and
+/// <https://drafts.csswg.org/css-images/#natural-dimensions> “In general, […]”
+///
+/// * For SVG, see <https://svgwg.org/svg2-draft/coords.html#SizingSVGInCSS>
+/// and again <https://github.com/w3c/csswg-drafts/issues/4572>.
+///
+/// * IFrames do not have natural width and height or natural ratio according
+/// to <https://drafts.csswg.org/css-images/#intrinsic-dimensions>.
+#[derive(Debug, MallocSizeOf)]
+pub(crate) struct NaturalSizes {
+ pub width: Option<Au>,
+ pub height: Option<Au>,
+ pub ratio: Option<CSSFloat>,
+}
+
+impl NaturalSizes {
+ pub(crate) fn from_width_and_height(width: f32, height: f32) -> Self {
+ // https://drafts.csswg.org/css-images/#natural-aspect-ratio:
+ // "If an object has a degenerate natural aspect ratio (at least one part being
+ // zero or infinity), it is treated as having no natural aspect ratio.""
+ let ratio = if width.is_normal() && height.is_normal() {
+ Some(width / height)
+ } else {
+ None
+ };
+
+ Self {
+ width: Some(Au::from_f32_px(width)),
+ height: Some(Au::from_f32_px(height)),
+ ratio,
+ }
+ }
+
+ pub(crate) fn empty() -> Self {
+ Self {
+ width: None,
+ height: None,
+ ratio: None,
+ }
+ }
+}
+
+#[derive(MallocSizeOf)]
+pub(crate) enum CanvasSource {
+ WebGL(ImageKey),
+ Image(ImageKey),
+ WebGPU(ImageKey),
+ /// transparent black
+ Empty,
+}
+
+impl fmt::Debug for CanvasSource {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}",
+ match *self {
+ CanvasSource::WebGL(_) => "WebGL",
+ CanvasSource::Image(_) => "Image",
+ CanvasSource::WebGPU(_) => "WebGPU",
+ CanvasSource::Empty => "Empty",
+ }
+ )
+ }
+}
+
+#[derive(Debug, MallocSizeOf)]
+pub(crate) struct CanvasInfo {
+ pub source: CanvasSource,
+}
+
+#[derive(Debug, MallocSizeOf)]
+pub(crate) struct IFrameInfo {
+ pub pipeline_id: PipelineId,
+ pub browsing_context_id: BrowsingContextId,
+}
+
+#[derive(Debug, MallocSizeOf)]
+pub(crate) struct VideoInfo {
+ pub image_key: webrender_api::ImageKey,
+}
+
+#[derive(Debug, MallocSizeOf)]
+pub(crate) enum ReplacedContentKind {
+ Image(#[conditional_malloc_size_of] Option<Arc<Image>>),
+ IFrame(IFrameInfo),
+ Canvas(CanvasInfo),
+ Video(Option<VideoInfo>),
+}
+
+impl ReplacedContents {
+ pub fn for_element<'dom>(element: impl NodeExt<'dom>, context: &LayoutContext) -> Option<Self> {
+ if let Some(ref data_attribute_string) = element.as_typeless_object_with_data_attribute() {
+ if let Some(url) = try_to_parse_image_data_url(data_attribute_string) {
+ return Self::from_image_url(
+ element,
+ context,
+ &ComputedUrl::Valid(ServoArc::new(url)),
+ );
+ }
+ }
+
+ let (kind, natural_size_in_dots) = {
+ if let Some((image, natural_size_in_dots)) = element.as_image() {
+ (
+ ReplacedContentKind::Image(image),
+ Some(natural_size_in_dots),
+ )
+ } else if let Some((canvas_info, natural_size_in_dots)) = element.as_canvas() {
+ (
+ ReplacedContentKind::Canvas(canvas_info),
+ Some(natural_size_in_dots),
+ )
+ } else if let Some((pipeline_id, browsing_context_id)) = element.as_iframe() {
+ (
+ ReplacedContentKind::IFrame(IFrameInfo {
+ pipeline_id,
+ browsing_context_id,
+ }),
+ None,
+ )
+ } else if let Some((image_key, natural_size_in_dots)) = element.as_video() {
+ (
+ ReplacedContentKind::Video(image_key.map(|key| VideoInfo { image_key: key })),
+ natural_size_in_dots,
+ )
+ } else {
+ return None;
+ }
+ };
+
+ if let ReplacedContentKind::Image(Some(ref image)) = kind {
+ context.handle_animated_image(element.opaque(), image.clone());
+ }
+
+ let natural_size = if let Some(naturalc_size_in_dots) = natural_size_in_dots {
+ // FIXME: should 'image-resolution' (when implemented) be used *instead* of
+ // `script::dom::htmlimageelement::ImageRequest::current_pixel_density`?
+ // https://drafts.csswg.org/css-images-4/#the-image-resolution
+ let dppx = 1.0;
+ let width = (naturalc_size_in_dots.width as CSSFloat) / dppx;
+ let height = (naturalc_size_in_dots.height as CSSFloat) / dppx;
+ NaturalSizes::from_width_and_height(width, height)
+ } else {
+ NaturalSizes::empty()
+ };
+
+ let base_fragment_info = BaseFragmentInfo::new_for_node(element.opaque());
+ Some(Self {
+ kind,
+ natural_size,
+ base_fragment_info,
+ })
+ }
+
+ pub fn from_image_url<'dom>(
+ element: impl NodeExt<'dom>,
+ context: &LayoutContext,
+ image_url: &ComputedUrl,
+ ) -> Option<Self> {
+ if let ComputedUrl::Valid(image_url) = image_url {
+ let (image, width, height) = match context.get_or_request_image_or_meta(
+ element.opaque(),
+ image_url.clone().into(),
+ UsePlaceholder::No,
+ ) {
+ Some(ImageOrMetadataAvailable::ImageAvailable { image, .. }) => {
+ (Some(image.clone()), image.width as f32, image.height as f32)
+ },
+ Some(ImageOrMetadataAvailable::MetadataAvailable(metadata, _id)) => {
+ (None, metadata.width as f32, metadata.height as f32)
+ },
+ None => return None,
+ };
+
+ return Some(Self {
+ kind: ReplacedContentKind::Image(image),
+ natural_size: NaturalSizes::from_width_and_height(width, height),
+ base_fragment_info: BaseFragmentInfo::new_for_node(element.opaque()),
+ });
+ }
+ None
+ }
+
+ pub fn from_image<'dom>(
+ element: impl NodeExt<'dom>,
+ context: &LayoutContext,
+ image: &ComputedImage,
+ ) -> Option<Self> {
+ match image {
+ ComputedImage::Url(image_url) => Self::from_image_url(element, context, image_url),
+ _ => None, // TODO
+ }
+ }
+
+ fn flow_relative_natural_size(&self, writing_mode: WritingMode) -> LogicalVec2<Option<Au>> {
+ let natural_size = PhysicalSize::new(self.natural_size.width, self.natural_size.height);
+ LogicalVec2::from_physical_size(&natural_size, writing_mode)
+ }
+
+ fn inline_size_over_block_size_intrinsic_ratio(
+ &self,
+ style: &ComputedValues,
+ ) -> Option<CSSFloat> {
+ self.natural_size.ratio.map(|width_over_height| {
+ if style.writing_mode.is_vertical() {
+ 1. / width_over_height
+ } else {
+ width_over_height
+ }
+ })
+ }
+
+ #[inline]
+ fn content_size(
+ &self,
+ axis: Direction,
+ preferred_aspect_ratio: Option<AspectRatio>,
+ get_size_in_opposite_axis: &dyn Fn() -> SizeConstraint,
+ get_fallback_size: &dyn Fn() -> Au,
+ ) -> Au {
+ let Some(ratio) = preferred_aspect_ratio else {
+ return get_fallback_size();
+ };
+ let transfer = |size| ratio.compute_dependent_size(axis, size);
+ match get_size_in_opposite_axis() {
+ SizeConstraint::Definite(size) => transfer(size),
+ SizeConstraint::MinMax(min_size, max_size) => get_fallback_size()
+ .clamp_between_extremums(transfer(min_size), max_size.map(transfer)),
+ }
+ }
+
+ pub fn make_fragments(
+ &self,
+ layout_context: &LayoutContext,
+ style: &ServoArc<ComputedValues>,
+ size: PhysicalSize<Au>,
+ ) -> Vec<Fragment> {
+ let natural_size = PhysicalSize::new(
+ self.natural_size.width.unwrap_or(size.width),
+ self.natural_size.height.unwrap_or(size.height),
+ );
+
+ let object_fit_size = self.natural_size.ratio.map_or(size, |width_over_height| {
+ let preserve_aspect_ratio_with_comparison =
+ |size: PhysicalSize<Au>, comparison: fn(&Au, &Au) -> bool| {
+ let candidate_width = size.height.scale_by(width_over_height);
+ if comparison(&candidate_width, &size.width) {
+ return PhysicalSize::new(candidate_width, size.height);
+ }
+
+ let candidate_height = size.width.scale_by(1. / width_over_height);
+ debug_assert!(comparison(&candidate_height, &size.height));
+ PhysicalSize::new(size.width, candidate_height)
+ };
+
+ match style.clone_object_fit() {
+ ObjectFit::Fill => size,
+ ObjectFit::Contain => preserve_aspect_ratio_with_comparison(size, PartialOrd::le),
+ ObjectFit::Cover => preserve_aspect_ratio_with_comparison(size, PartialOrd::ge),
+ ObjectFit::None => natural_size,
+ ObjectFit::ScaleDown => {
+ preserve_aspect_ratio_with_comparison(size.min(natural_size), PartialOrd::le)
+ },
+ }
+ });
+
+ let object_position = style.clone_object_position();
+ let horizontal_position = object_position
+ .horizontal
+ .to_used_value(size.width - object_fit_size.width);
+ let vertical_position = object_position
+ .vertical
+ .to_used_value(size.height - object_fit_size.height);
+
+ let rect = PhysicalRect::new(
+ PhysicalPoint::new(horizontal_position, vertical_position),
+ object_fit_size,
+ );
+ let clip = PhysicalRect::new(PhysicalPoint::origin(), size);
+
+ match &self.kind {
+ ReplacedContentKind::Image(image) => image
+ .as_ref()
+ .and_then(|image| image.id)
+ .map(|image_key| {
+ Fragment::Image(ArcRefCell::new(ImageFragment {
+ base: self.base_fragment_info.into(),
+ style: style.clone(),
+ rect,
+ clip,
+ image_key: Some(image_key),
+ }))
+ })
+ .into_iter()
+ .collect(),
+ ReplacedContentKind::Video(video) => {
+ vec![Fragment::Image(ArcRefCell::new(ImageFragment {
+ base: self.base_fragment_info.into(),
+ style: style.clone(),
+ rect,
+ clip,
+ image_key: video.as_ref().map(|video| video.image_key),
+ }))]
+ },
+ ReplacedContentKind::IFrame(iframe) => {
+ let size = Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px());
+ let hidpi_scale_factor = layout_context.shared_context().device_pixel_ratio();
+
+ layout_context.iframe_sizes.lock().insert(
+ iframe.browsing_context_id,
+ IFrameSize {
+ browsing_context_id: iframe.browsing_context_id,
+ pipeline_id: iframe.pipeline_id,
+ viewport_details: ViewportDetails {
+ size,
+ hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
+ },
+ },
+ );
+ vec![Fragment::IFrame(ArcRefCell::new(IFrameFragment {
+ base: self.base_fragment_info.into(),
+ style: style.clone(),
+ pipeline_id: iframe.pipeline_id,
+ rect,
+ }))]
+ },
+ ReplacedContentKind::Canvas(canvas_info) => {
+ if self.natural_size.width == Some(Au::zero()) ||
+ self.natural_size.height == Some(Au::zero())
+ {
+ return vec![];
+ }
+
+ let image_key = match canvas_info.source {
+ CanvasSource::WebGL(image_key) => image_key,
+ CanvasSource::WebGPU(image_key) => image_key,
+ CanvasSource::Image(image_key) => image_key,
+ CanvasSource::Empty => return vec![],
+ };
+ vec![Fragment::Image(ArcRefCell::new(ImageFragment {
+ base: self.base_fragment_info.into(),
+ style: style.clone(),
+ rect,
+ clip,
+ image_key: Some(image_key),
+ }))]
+ },
+ }
+ }
+
+ pub(crate) fn preferred_aspect_ratio(
+ &self,
+ style: &ComputedValues,
+ padding_border_sums: &LogicalVec2<Au>,
+ ) -> Option<AspectRatio> {
+ style
+ .preferred_aspect_ratio(
+ self.inline_size_over_block_size_intrinsic_ratio(style),
+ padding_border_sums,
+ )
+ .or_else(|| {
+ matches!(self.kind, ReplacedContentKind::Video(_)).then(|| {
+ let size = Self::default_object_size();
+ AspectRatio::from_content_ratio(
+ size.width.to_f32_px() / size.height.to_f32_px(),
+ )
+ })
+ })
+ }
+
+ /// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-width>
+ /// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-height>
+ ///
+ /// Also used in other cases, for example
+ /// <https://drafts.csswg.org/css2/visudet.html#block-replaced-width>
+ pub(crate) fn used_size_as_if_inline_element(
+ &self,
+ containing_block: &ContainingBlock,
+ style: &ComputedValues,
+ content_box_sizes_and_pbm: &ContentBoxSizesAndPBM,
+ ignore_block_margins_for_stretch: LogicalSides1D<bool>,
+ ) -> LogicalVec2<Au> {
+ let pbm = &content_box_sizes_and_pbm.pbm;
+ self.used_size_as_if_inline_element_from_content_box_sizes(
+ containing_block,
+ style,
+ self.preferred_aspect_ratio(style, &pbm.padding_border_sums),
+ content_box_sizes_and_pbm.content_box_sizes.as_ref(),
+ Size::FitContent.into(),
+ pbm.sums_auto_is_zero(ignore_block_margins_for_stretch),
+ )
+ }
+
+ pub(crate) fn default_object_size() -> PhysicalSize<Au> {
+ // FIXME:
+ // https://drafts.csswg.org/css-images/#default-object-size
+ // “If 300px is too wide to fit the device, UAs should use the width of
+ // the largest rectangle that has a 2:1 ratio and fits the device instead.”
+ // “height of the largest rectangle that has a 2:1 ratio, has a height not greater
+ // than 150px, and has a width not greater than the device width.”
+ PhysicalSize::new(Au::from_px(300), Au::from_px(150))
+ }
+
+ pub(crate) fn flow_relative_default_object_size(writing_mode: WritingMode) -> LogicalVec2<Au> {
+ LogicalVec2::from_physical_size(&Self::default_object_size(), writing_mode)
+ }
+
+ /// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-width>
+ /// <https://drafts.csswg.org/css2/visudet.html#inline-replaced-height>
+ ///
+ /// Also used in other cases, for example
+ /// <https://drafts.csswg.org/css2/visudet.html#block-replaced-width>
+ ///
+ /// The logic differs from CSS2 in order to properly handle `aspect-ratio` and keyword sizes.
+ /// Each axis can have preferred, min and max sizing constraints, plus constraints transferred
+ /// from the other axis if there is an aspect ratio, plus a natural and default size.
+ /// In case of conflict, the order of precedence (from highest to lowest) is:
+ /// 1. Non-transferred min constraint
+ /// 2. Non-transferred max constraint
+ /// 3. Non-transferred preferred constraint
+ /// 4. Transferred min constraint
+ /// 5. Transferred max constraint
+ /// 6. Transferred preferred constraint
+ /// 7. Natural size
+ /// 8. Default object size
+ ///
+ /// <https://drafts.csswg.org/css-sizing-4/#aspect-ratio-size-transfers>
+ /// <https://github.com/w3c/csswg-drafts/issues/6071#issuecomment-2243986313>
+ pub(crate) fn used_size_as_if_inline_element_from_content_box_sizes(
+ &self,
+ containing_block: &ContainingBlock,
+ style: &ComputedValues,
+ preferred_aspect_ratio: Option<AspectRatio>,
+ sizes: LogicalVec2<&Sizes>,
+ automatic_size: LogicalVec2<Size<Au>>,
+ pbm_sums: LogicalVec2<Au>,
+ ) -> LogicalVec2<Au> {
+ // <https://drafts.csswg.org/css-images-3/#natural-dimensions>
+ // <https://drafts.csswg.org/css-images-3/#default-object-size>
+ let writing_mode = style.writing_mode;
+ let natural_size = LazyCell::new(|| self.flow_relative_natural_size(writing_mode));
+ let default_object_size =
+ LazyCell::new(|| Self::flow_relative_default_object_size(writing_mode));
+ let get_inline_fallback_size = || {
+ natural_size
+ .inline
+ .unwrap_or_else(|| default_object_size.inline)
+ };
+ let get_block_fallback_size = || {
+ natural_size
+ .block
+ .unwrap_or_else(|| default_object_size.block)
+ };
+
+ // <https://drafts.csswg.org/css-sizing-4/#stretch-fit-sizing>
+ let inline_stretch_size = Au::zero().max(containing_block.size.inline - pbm_sums.inline);
+ let block_stretch_size = containing_block
+ .size
+ .block
+ .to_definite()
+ .map(|block_size| Au::zero().max(block_size - pbm_sums.block));
+
+ // First, compute the inline size. Intrinsic values depend on the block sizing properties
+ // through the aspect ratio, but these can also be intrinsic and depend on the inline size.
+ // Therefore, we tentatively treat intrinsic block sizing properties as their initial value.
+ let get_inline_content_size = || {
+ let get_block_size = || {
+ sizes
+ .block
+ .resolve_extrinsic(automatic_size.block, Au::zero(), block_stretch_size)
+ };
+ self.content_size(
+ Direction::Inline,
+ preferred_aspect_ratio,
+ &get_block_size,
+ &get_inline_fallback_size,
+ )
+ .into()
+ };
+ let inline_size = sizes.inline.resolve(
+ Direction::Inline,
+ automatic_size.inline,
+ Au::zero,
+ Some(inline_stretch_size),
+ get_inline_content_size,
+ false, /* is_table */
+ );
+
+ // Now we can compute the block size, using the inline size from above.
+ let block_content_size = LazyCell::new(|| -> ContentSizes {
+ let get_inline_size = || SizeConstraint::Definite(inline_size);
+ self.content_size(
+ Direction::Block,
+ preferred_aspect_ratio,
+ &get_inline_size,
+ &get_block_fallback_size,
+ )
+ .into()
+ });
+ let block_size = sizes.block.resolve(
+ Direction::Block,
+ automatic_size.block,
+ Au::zero,
+ block_stretch_size,
+ || *block_content_size,
+ false, /* is_table */
+ );
+
+ LogicalVec2 {
+ inline: inline_size,
+ block: block_size,
+ }
+ }
+
+ #[inline]
+ pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
+ LayoutStyle::Default(&base.style)
+ }
+}
+
+impl ComputeInlineContentSizes for ReplacedContents {
+ fn compute_inline_content_sizes(
+ &self,
+ _: &LayoutContext,
+ constraint_space: &ConstraintSpace,
+ ) -> InlineContentSizesResult {
+ let get_inline_fallback_size = || {
+ let writing_mode = constraint_space.writing_mode;
+ self.flow_relative_natural_size(writing_mode)
+ .inline
+ .unwrap_or_else(|| Self::flow_relative_default_object_size(writing_mode).inline)
+ };
+ let inline_content_size = self.content_size(
+ Direction::Inline,
+ constraint_space.preferred_aspect_ratio,
+ &|| constraint_space.block_size,
+ &get_inline_fallback_size,
+ );
+ InlineContentSizesResult {
+ sizes: inline_content_size.into(),
+ depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(),
+ }
+ }
+}
+
+fn try_to_parse_image_data_url(string: &str) -> Option<Url> {
+ if !string.starts_with("data:") {
+ return None;
+ }
+ let data_url = DataUrl::process(string).ok()?;
+ let mime_type = data_url.mime_type();
+ if mime_type.type_ != "image" {
+ return None;
+ }
+
+ // TODO: Find a better way to test for supported image formats. Currently this type of check is
+ // repeated several places in Servo, but should be centralized somehow.
+ if !matches!(
+ mime_type.subtype.as_str(),
+ "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico"
+ ) {
+ return None;
+ }
+
+ Url::parse(string).ok()
+}