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