layout/
replaced.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use app_units::{Au, MAX_AU};
6use base::id::{BrowsingContextId, PipelineId};
7use data_url::DataUrl;
8use embedder_traits::ViewportDetails;
9use euclid::{Scale, Size2D};
10use layout_api::wrapper_traits::ThreadSafeLayoutNode;
11use layout_api::{IFrameSize, LayoutImageDestination};
12use malloc_size_of_derive::MallocSizeOf;
13use net_traits::image_cache::{Image, ImageOrMetadataAvailable, VectorImage};
14use script::layout_dom::ServoThreadSafeLayoutNode;
15use servo_arc::Arc as ServoArc;
16use style::Zero;
17use style::computed_values::object_fit::T as ObjectFit;
18use style::logical_geometry::{Direction, WritingMode};
19use style::properties::ComputedValues;
20use style::servo::url::ComputedUrl;
21use style::values::CSSFloat;
22use style::values::computed::image::Image as ComputedImage;
23use url::Url;
24use webrender_api::ImageKey;
25
26use crate::cell::ArcRefCell;
27use crate::context::{LayoutContext, LayoutImageCacheResult};
28use crate::dom::NodeExt;
29use crate::fragment_tree::{
30    BaseFragmentInfo, CollapsedBlockMargins, Fragment, IFrameFragment, ImageFragment,
31};
32use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
33use crate::layout_box_base::{CacheableLayoutResult, LayoutBoxBase};
34use crate::sizing::{
35    ComputeInlineContentSizes, InlineContentSizesResult, LazySize, SizeConstraint,
36};
37use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, LayoutStyle};
38use crate::{ConstraintSpace, ContainingBlock};
39
40#[derive(Debug, MallocSizeOf)]
41pub(crate) struct ReplacedContents {
42    pub kind: ReplacedContentKind,
43    natural_size: NaturalSizes,
44    base_fragment_info: BaseFragmentInfo,
45}
46
47/// The natural dimensions of a replaced element, including a height, width, and
48/// aspect ratio.
49///
50/// * Raster images always have an natural width and height, with 1 image pixel = 1px.
51///   The natural ratio should be based on dividing those.
52///   See <https://github.com/w3c/csswg-drafts/issues/4572> for the case where either is zero.
53///   PNG specifically disallows this but I (SimonSapin) am not sure about other formats.
54///
55/// * Form controls have both natural width and height **but no natural ratio**.
56///   See <https://github.com/w3c/csswg-drafts/issues/1044> and
57///   <https://drafts.csswg.org/css-images/#natural-dimensions> “In general, […]”
58///
59/// * For SVG, see <https://svgwg.org/svg2-draft/coords.html#SizingSVGInCSS>
60///   and again <https://github.com/w3c/csswg-drafts/issues/4572>.
61///
62/// * IFrames do not have natural width and height or natural ratio according
63///   to <https://drafts.csswg.org/css-images/#intrinsic-dimensions>.
64#[derive(Debug, MallocSizeOf)]
65pub(crate) struct NaturalSizes {
66    pub width: Option<Au>,
67    pub height: Option<Au>,
68    pub ratio: Option<CSSFloat>,
69}
70
71impl NaturalSizes {
72    pub(crate) fn from_width_and_height(width: f32, height: f32) -> Self {
73        // https://drafts.csswg.org/css-images/#natural-aspect-ratio:
74        // "If an object has a degenerate natural aspect ratio (at least one part being
75        // zero or infinity), it is treated as having no natural aspect ratio.""
76        let ratio = if width.is_normal() && height.is_normal() {
77            Some(width / height)
78        } else {
79            None
80        };
81
82        Self {
83            width: Some(Au::from_f32_px(width)),
84            height: Some(Au::from_f32_px(height)),
85            ratio,
86        }
87    }
88
89    pub(crate) fn from_natural_size_in_dots(natural_size_in_dots: PhysicalSize<f64>) -> Self {
90        // FIXME: should 'image-resolution' (when implemented) be used *instead* of
91        // `script::dom::htmlimageelement::ImageRequest::current_pixel_density`?
92        // https://drafts.csswg.org/css-images-4/#the-image-resolution
93        let dppx = 1.0;
94        let width = natural_size_in_dots.width as f32 / dppx;
95        let height = natural_size_in_dots.height as f32 / dppx;
96        Self::from_width_and_height(width, height)
97    }
98
99    pub(crate) fn empty() -> Self {
100        Self {
101            width: None,
102            height: None,
103            ratio: None,
104        }
105    }
106}
107
108#[derive(Debug, MallocSizeOf)]
109pub(crate) struct CanvasInfo {
110    pub source: Option<ImageKey>,
111}
112
113#[derive(Debug, MallocSizeOf)]
114pub(crate) struct IFrameInfo {
115    pub pipeline_id: PipelineId,
116    pub browsing_context_id: BrowsingContextId,
117}
118
119#[derive(Debug, MallocSizeOf)]
120pub(crate) struct VideoInfo {
121    pub image_key: webrender_api::ImageKey,
122}
123
124#[derive(Debug, MallocSizeOf)]
125pub(crate) enum ReplacedContentKind {
126    Image(Option<Image>, bool /* showing_broken_image_icon */),
127    IFrame(IFrameInfo),
128    Canvas(CanvasInfo),
129    Video(Option<VideoInfo>),
130    SVGElement(Option<VectorImage>),
131}
132
133impl ReplacedContents {
134    pub fn for_element(
135        node: ServoThreadSafeLayoutNode<'_>,
136        context: &LayoutContext,
137    ) -> Option<Self> {
138        if let Some(ref data_attribute_string) = node.as_typeless_object_with_data_attribute() {
139            if let Some(url) = try_to_parse_image_data_url(data_attribute_string) {
140                return Self::from_image_url(
141                    node,
142                    context,
143                    &ComputedUrl::Valid(ServoArc::new(url)),
144                );
145            }
146        }
147
148        let (kind, natural_size) = {
149            if let Some((image, natural_size_in_dots)) = node.as_image() {
150                (
151                    ReplacedContentKind::Image(image, node.showing_broken_image_icon()),
152                    NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
153                )
154            } else if let Some((canvas_info, natural_size_in_dots)) = node.as_canvas() {
155                (
156                    ReplacedContentKind::Canvas(canvas_info),
157                    NaturalSizes::from_natural_size_in_dots(natural_size_in_dots),
158                )
159            } else if let Some((pipeline_id, browsing_context_id)) = node.as_iframe() {
160                (
161                    ReplacedContentKind::IFrame(IFrameInfo {
162                        pipeline_id,
163                        browsing_context_id,
164                    }),
165                    NaturalSizes::empty(),
166                )
167            } else if let Some((image_key, natural_size_in_dots)) = node.as_video() {
168                (
169                    ReplacedContentKind::Video(image_key.map(|key| VideoInfo { image_key: key })),
170                    natural_size_in_dots
171                        .map_or_else(NaturalSizes::empty, NaturalSizes::from_natural_size_in_dots),
172                )
173            } else if let Some(svg_data) = node.as_svg() {
174                let svg_source = match svg_data.source {
175                    None => {
176                        // The SVGSVGElement is not yet serialized, so we add it to a list
177                        // and hand it over to script to peform the serialization.
178                        context
179                            .image_resolver
180                            .queue_svg_element_for_serialization(node);
181                        return None;
182                    },
183                    Some(Err(_)) => {
184                        // Don't attempt to serialize if previous attempt had errored.
185                        return None;
186                    },
187                    Some(Ok(svg_source)) => svg_source,
188                };
189
190                let result = context
191                    .image_resolver
192                    .get_cached_image_for_url(
193                        node.opaque(),
194                        svg_source,
195                        LayoutImageDestination::BoxTreeConstruction,
196                    )
197                    .ok();
198
199                let vector_image = result.map(|result| match result {
200                    Image::Vector(vector_image) => vector_image,
201                    _ => unreachable!("SVG element can't contain a raster image."),
202                });
203                let natural_size = NaturalSizes {
204                    width: svg_data.width.map(Au::from_px),
205                    height: svg_data.height.map(Au::from_px),
206                    ratio: svg_data.ratio,
207                };
208                (ReplacedContentKind::SVGElement(vector_image), natural_size)
209            } else {
210                return None;
211            }
212        };
213
214        if let ReplacedContentKind::Image(Some(Image::Raster(ref image)), _) = kind {
215            context
216                .image_resolver
217                .handle_animated_image(node.opaque(), image.clone());
218        }
219
220        Some(Self {
221            kind,
222            natural_size,
223            base_fragment_info: node.into(),
224        })
225    }
226
227    pub fn from_image_url(
228        node: ServoThreadSafeLayoutNode<'_>,
229        context: &LayoutContext,
230        image_url: &ComputedUrl,
231    ) -> Option<Self> {
232        if let ComputedUrl::Valid(image_url) = image_url {
233            let (image, width, height) = match context.image_resolver.get_or_request_image_or_meta(
234                node.opaque(),
235                image_url.clone().into(),
236                LayoutImageDestination::BoxTreeConstruction,
237            ) {
238                LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta {
239                    ImageOrMetadataAvailable::ImageAvailable { image, .. } => {
240                        let metadata = image.metadata();
241                        (
242                            Some(image.clone()),
243                            metadata.width as f32,
244                            metadata.height as f32,
245                        )
246                    },
247                    ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => {
248                        (None, metadata.width as f32, metadata.height as f32)
249                    },
250                },
251                LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None,
252            };
253
254            return Some(Self {
255                kind: ReplacedContentKind::Image(image, false /* showing_broken_image_icon */),
256                natural_size: NaturalSizes::from_width_and_height(width, height),
257                base_fragment_info: node.into(),
258            });
259        }
260        None
261    }
262
263    pub fn from_image(
264        element: ServoThreadSafeLayoutNode<'_>,
265        context: &LayoutContext,
266        image: &ComputedImage,
267    ) -> Option<Self> {
268        match image {
269            ComputedImage::Url(image_url) => Self::from_image_url(element, context, image_url),
270            _ => None, // TODO
271        }
272    }
273
274    #[inline]
275    fn is_broken_image(&self) -> bool {
276        matches!(self.kind, ReplacedContentKind::Image(_, true))
277    }
278
279    #[inline]
280    fn content_size(
281        &self,
282        axis: Direction,
283        preferred_aspect_ratio: Option<AspectRatio>,
284        get_size_in_opposite_axis: &dyn Fn() -> SizeConstraint,
285        get_fallback_size: &dyn Fn() -> Au,
286    ) -> Au {
287        let Some(ratio) = preferred_aspect_ratio else {
288            return get_fallback_size();
289        };
290        let transfer = |size| ratio.compute_dependent_size(axis, size);
291        match get_size_in_opposite_axis() {
292            SizeConstraint::Definite(size) => transfer(size),
293            SizeConstraint::MinMax(min_size, max_size) => get_fallback_size()
294                .clamp_between_extremums(transfer(min_size), max_size.map(transfer)),
295        }
296    }
297
298    fn calculate_fragment_rect(
299        &self,
300        style: &ServoArc<ComputedValues>,
301        size: PhysicalSize<Au>,
302    ) -> (PhysicalSize<Au>, PhysicalRect<Au>) {
303        if let ReplacedContentKind::Image(Some(Image::Raster(image)), true) = &self.kind {
304            let size = Size2D::new(
305                Au::from_f32_px(image.metadata.width as f32),
306                Au::from_f32_px(image.metadata.height as f32),
307            )
308            .min(size);
309            return (PhysicalSize::zero(), size.into());
310        }
311
312        let natural_size = PhysicalSize::new(
313            self.natural_size.width.unwrap_or(size.width),
314            self.natural_size.height.unwrap_or(size.height),
315        );
316
317        let object_fit_size = self.natural_size.ratio.map_or(size, |width_over_height| {
318            let preserve_aspect_ratio_with_comparison =
319                |size: PhysicalSize<Au>, comparison: fn(&Au, &Au) -> bool| {
320                    let candidate_width = size.height.scale_by(width_over_height);
321                    if comparison(&candidate_width, &size.width) {
322                        return PhysicalSize::new(candidate_width, size.height);
323                    }
324
325                    let candidate_height = size.width.scale_by(1. / width_over_height);
326                    debug_assert!(comparison(&candidate_height, &size.height));
327                    PhysicalSize::new(size.width, candidate_height)
328                };
329
330            match style.clone_object_fit() {
331                ObjectFit::Fill => size,
332                ObjectFit::Contain => preserve_aspect_ratio_with_comparison(size, PartialOrd::le),
333                ObjectFit::Cover => preserve_aspect_ratio_with_comparison(size, PartialOrd::ge),
334                ObjectFit::None => natural_size,
335                ObjectFit::ScaleDown => {
336                    preserve_aspect_ratio_with_comparison(size.min(natural_size), PartialOrd::le)
337                },
338            }
339        });
340
341        let object_position = style.clone_object_position();
342        let horizontal_position = object_position
343            .horizontal
344            .to_used_value(size.width - object_fit_size.width);
345        let vertical_position = object_position
346            .vertical
347            .to_used_value(size.height - object_fit_size.height);
348
349        let object_position = PhysicalPoint::new(horizontal_position, vertical_position);
350        (
351            object_fit_size,
352            PhysicalRect::new(object_position, object_fit_size),
353        )
354    }
355
356    pub fn make_fragments(
357        &self,
358        layout_context: &LayoutContext,
359        style: &ServoArc<ComputedValues>,
360        size: PhysicalSize<Au>,
361    ) -> Vec<Fragment> {
362        let (object_fit_size, rect) = self.calculate_fragment_rect(style, size);
363        let clip = PhysicalRect::new(PhysicalPoint::origin(), size);
364
365        match &self.kind {
366            ReplacedContentKind::Image(image, showing_broken_image_icon) => image
367                .as_ref()
368                .and_then(|image| match image {
369                    Image::Raster(raster_image) => raster_image.id,
370                    Image::Vector(vector_image) => {
371                        let scale = layout_context.style_context.device_pixel_ratio();
372                        let width = object_fit_size.width.scale_by(scale.0).to_px();
373                        let height = object_fit_size.height.scale_by(scale.0).to_px();
374                        let size = Size2D::new(width, height);
375                        let tag = self.base_fragment_info.tag?;
376                        layout_context
377                            .image_resolver
378                            .rasterize_vector_image(vector_image.id, size, tag.node)
379                            .and_then(|i| i.id)
380                    },
381                })
382                .map(|image_key| {
383                    Fragment::Image(ArcRefCell::new(ImageFragment {
384                        base: self.base_fragment_info.into(),
385                        style: style.clone(),
386                        rect,
387                        clip,
388                        image_key: Some(image_key),
389                        showing_broken_image_icon: *showing_broken_image_icon,
390                    }))
391                })
392                .into_iter()
393                .collect(),
394            ReplacedContentKind::Video(video) => {
395                vec![Fragment::Image(ArcRefCell::new(ImageFragment {
396                    base: self.base_fragment_info.into(),
397                    style: style.clone(),
398                    rect,
399                    clip,
400                    image_key: video.as_ref().map(|video| video.image_key),
401                    showing_broken_image_icon: false,
402                }))]
403            },
404            ReplacedContentKind::IFrame(iframe) => {
405                let size = Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px());
406                let hidpi_scale_factor = layout_context.style_context.device_pixel_ratio();
407
408                layout_context.iframe_sizes.lock().insert(
409                    iframe.browsing_context_id,
410                    IFrameSize {
411                        browsing_context_id: iframe.browsing_context_id,
412                        pipeline_id: iframe.pipeline_id,
413                        viewport_details: ViewportDetails {
414                            size,
415                            hidpi_scale_factor: Scale::new(hidpi_scale_factor.0),
416                        },
417                    },
418                );
419                vec![Fragment::IFrame(ArcRefCell::new(IFrameFragment {
420                    base: self.base_fragment_info.into(),
421                    style: style.clone(),
422                    pipeline_id: iframe.pipeline_id,
423                    rect,
424                }))]
425            },
426            ReplacedContentKind::Canvas(canvas_info) => {
427                if self.natural_size.width == Some(Au::zero()) ||
428                    self.natural_size.height == Some(Au::zero())
429                {
430                    return vec![];
431                }
432
433                let Some(image_key) = canvas_info.source else {
434                    return vec![];
435                };
436
437                vec![Fragment::Image(ArcRefCell::new(ImageFragment {
438                    base: self.base_fragment_info.into(),
439                    style: style.clone(),
440                    rect,
441                    clip,
442                    image_key: Some(image_key),
443                    showing_broken_image_icon: false,
444                }))]
445            },
446            ReplacedContentKind::SVGElement(vector_image) => {
447                let Some(vector_image) = vector_image else {
448                    return vec![];
449                };
450                let scale = layout_context.style_context.device_pixel_ratio();
451                // TODO: This is incorrect if the SVG has a viewBox.
452                let size = PhysicalSize::new(
453                    vector_image
454                        .metadata
455                        .width
456                        .try_into()
457                        .map_or(MAX_AU, Au::from_px),
458                    vector_image
459                        .metadata
460                        .height
461                        .try_into()
462                        .map_or(MAX_AU, Au::from_px),
463                );
464                let rect = PhysicalRect::from_size(size);
465                let raster_size = Size2D::new(
466                    size.width.scale_by(scale.0).to_px(),
467                    size.height.scale_by(scale.0).to_px(),
468                );
469                let tag = self.base_fragment_info.tag.unwrap();
470                layout_context
471                    .image_resolver
472                    .rasterize_vector_image(vector_image.id, raster_size, tag.node)
473                    .and_then(|image| image.id)
474                    .map(|image_key| {
475                        Fragment::Image(ArcRefCell::new(ImageFragment {
476                            base: self.base_fragment_info.into(),
477                            style: style.clone(),
478                            rect,
479                            clip,
480                            image_key: Some(image_key),
481                            showing_broken_image_icon: false,
482                        }))
483                    })
484                    .into_iter()
485                    .collect()
486            },
487        }
488    }
489
490    pub(crate) fn preferred_aspect_ratio(
491        &self,
492        style: &ComputedValues,
493        padding_border_sums: &LogicalVec2<Au>,
494    ) -> Option<AspectRatio> {
495        if self.is_broken_image() {
496            // This isn't specified, but when an image is broken, we should prefer to the aspect
497            // ratio from the style, rather than the aspect ratio from the broken image icon.
498            // Note that the broken image icon *does* affect the content size of the image
499            // though as we want the image to be as big as the icon if the size was not specified
500            // in the style.
501            style.preferred_aspect_ratio(None, padding_border_sums)
502        } else {
503            style.preferred_aspect_ratio(self.natural_size.ratio, padding_border_sums)
504        }
505    }
506
507    /// The inline size that would result from combining the natural size
508    /// and the default object size, but disregarding the specified size.
509    /// <https://drafts.csswg.org/css-images-3/#natural-dimensions>
510    /// <https://drafts.csswg.org/css-images-3/#default-object-size>
511    /// <https://drafts.csswg.org/css-images-3/#specified-size>
512    pub(crate) fn fallback_inline_size(&self, writing_mode: WritingMode) -> Au {
513        if writing_mode.is_horizontal() {
514            self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
515        } else {
516            self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
517        }
518    }
519
520    /// The block size that would result from combining the natural size
521    /// and the default object size, but disregarding the specified size.
522    /// <https://drafts.csswg.org/css-images-3/#natural-dimensions>
523    /// <https://drafts.csswg.org/css-images-3/#default-object-size>
524    /// <https://drafts.csswg.org/css-images-3/#specified-size>
525    pub(crate) fn fallback_block_size(&self, writing_mode: WritingMode) -> Au {
526        if writing_mode.is_horizontal() {
527            self.natural_size.height.unwrap_or_else(|| Au::from_px(150))
528        } else {
529            self.natural_size.width.unwrap_or_else(|| Au::from_px(300))
530        }
531    }
532
533    #[inline]
534    pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> {
535        LayoutStyle::Default(&base.style)
536    }
537
538    pub(crate) fn layout(
539        &self,
540        layout_context: &LayoutContext,
541        containing_block_for_children: &ContainingBlock,
542        preferred_aspect_ratio: Option<AspectRatio>,
543        base: &LayoutBoxBase,
544        lazy_block_size: &LazySize,
545    ) -> CacheableLayoutResult {
546        let writing_mode = base.style.writing_mode;
547        let inline_size = containing_block_for_children.size.inline;
548        let content_block_size = self.content_size(
549            Direction::Block,
550            preferred_aspect_ratio,
551            &|| SizeConstraint::Definite(inline_size),
552            &|| self.fallback_block_size(writing_mode),
553        );
554        let size = LogicalVec2 {
555            inline: inline_size,
556            block: lazy_block_size.resolve(|| content_block_size),
557        }
558        .to_physical_size(writing_mode);
559        CacheableLayoutResult {
560            baselines: Default::default(),
561            collapsible_margins_in_children: CollapsedBlockMargins::zero(),
562            content_block_size,
563            content_inline_size_for_table: None,
564            // The result doesn't depend on `containing_block_for_children.size.block`,
565            // but it depends on `lazy_block_size`, which is probably tied to that.
566            depends_on_block_constraints: true,
567            fragments: self.make_fragments(layout_context, &base.style, size),
568            specific_layout_info: None,
569        }
570    }
571}
572
573impl ComputeInlineContentSizes for ReplacedContents {
574    fn compute_inline_content_sizes(
575        &self,
576        _: &LayoutContext,
577        constraint_space: &ConstraintSpace,
578    ) -> InlineContentSizesResult {
579        let inline_content_size = self.content_size(
580            Direction::Inline,
581            constraint_space.preferred_aspect_ratio,
582            &|| constraint_space.block_size,
583            &|| self.fallback_inline_size(constraint_space.style.writing_mode),
584        );
585        InlineContentSizesResult {
586            sizes: inline_content_size.into(),
587            depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(),
588        }
589    }
590}
591
592fn try_to_parse_image_data_url(string: &str) -> Option<Url> {
593    if !string.starts_with("data:") {
594        return None;
595    }
596    let data_url = DataUrl::process(string).ok()?;
597    let mime_type = data_url.mime_type();
598    if mime_type.type_ != "image" {
599        return None;
600    }
601
602    // TODO: Find a better way to test for supported image formats. Currently this type of check is
603    // repeated several places in Servo, but should be centralized somehow.
604    if !matches!(
605        mime_type.subtype.as_str(),
606        "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico"
607    ) {
608        return None;
609    }
610
611    Url::parse(string).ok()
612}