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