Skip to main content

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