/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/layout/replaced.rs
810 строк
31 KB
Shubham Gupta
layout: Add `effective_visual_size` to `PaintTimingHandler` (#43378)
07 авг 2026, 06:09
Не верифицирован
07 авг 2026, 06:09
30bba6d
Код
Авторство
О чём код?
/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::sync::Arc; use app_units::{Au, MAX_AU}; use data_url::DataUrl; use embedder_traits::ViewportDetails; use euclid::{Scale, Size2D}; use layout_api::{IFrameSize, LayoutElement, LayoutImageDestination, LayoutNode, SVGElementData}; use malloc_size_of_derive::MallocSizeOf; use net_traits::image_cache::{Image, ImageOrMetadataAvailable, VectorImage}; use net_traits::request::InternalRequest; use script::layout_dom::ServoLayoutNode; use servo_arc::Arc as ServoArc; use servo_base::id::{BrowsingContextId, PipelineId}; use servo_url::ServoUrl; use style::Zero; use style::attr::AttrValue; use style::computed_values::object_fit::T as ObjectFit; use style::context::TreeCountingCaches; use style::dom::DummyElementContext; use style::logical_geometry::{Direction, WritingMode}; use style::properties::{ComputedValues, StyleBuilder}; use style::rule_cache::RuleCacheConditions; use style::rule_tree::RuleCascadeFlags; use style::stylesheets::container_rule::ContainerSizeQuery; use style::url::ComputedUrl; use style::values::CSSFloat; use style::values::computed::image::Image as ComputedImage; use style::values::computed::{Content, Context, ToComputedValue}; use style::values::generics::counters::{GenericContentItem, GenericContentItems}; use url::Url; use web_atoms::local_name; use webrender_api::ImageKey; use crate::context::{LayoutContext, LayoutImageCacheResult}; use crate::dom::NodeExt; use crate::fragment_tree::{ BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment, IFrameFragment, ImageFragment, }; use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize}; use crate::layout_box_base::{IndependentFormattingContextLayoutResult, LayoutBoxBase}; use crate::sizing::{ ComputeInlineContentSizes, InlineContentSizesResult, LazySize, SizeConstraint, }; use crate::style_ext::{AspectRatio, Clamp, ComputedValuesExt, LayoutStyle}; use crate::{ConstraintSpace, ContainingBlock}; #[derive(Debug, MallocSizeOf)] pub(crate) struct ReplacedContents { pub kind: ReplacedContentKind, /// Whether or not this [`ReplacedContents`] is due to content replacement, i.e. /// `content: <image>` in style. pub is_content_replacement: bool, natural_size: NaturalSizes, base_fragment_info: BaseFragmentInfo, } /// The natural dimensions of a replaced element, including a height, width, and /// aspect ratio. /// /// * Raster images always have an natural width and height, with 1 image pixel = 1px. /// The natural ratio should be based on dividing those. /// See <https://github.com/w3c/csswg-drafts/issues/4572> for the case where either is zero. /// PNG specifically disallows this but I (SimonSapin) am not sure about other formats. /// /// * Form controls have both natural width and height **but no natural ratio**. /// See <https://github.com/w3c/csswg-drafts/issues/1044> and /// <https://drafts.csswg.org/css-images/#natural-dimensions> “In general, […]” /// /// * For SVG, see <https://svgwg.org/svg2-draft/coords.html#SizingSVGInCSS> /// and again <https://github.com/w3c/csswg-drafts/issues/4572>. /// /// * IFrames do not have natural width and height or natural ratio according /// to <https://drafts.csswg.org/css-images/#intrinsic-dimensions>. #[derive(Debug, MallocSizeOf)] pub(crate) struct NaturalSizes { pub width: Option<Au>, pub height: Option<Au>, pub ratio: Option<CSSFloat>, } impl NaturalSizes { pub(crate) fn from_width_and_height(width: f32, height: f32) -> Self { // https://drafts.csswg.org/css-images/#natural-aspect-ratio: // "If an object has a degenerate natural aspect ratio (at least one part being // zero or infinity), it is treated as having no natural aspect ratio."" let ratio = if width.is_normal() && height.is_normal() { Some(width / height) } else { None }; Self { width: Some(Au::from_f32_px(width)), height: Some(Au::from_f32_px(height)), ratio, } } pub(crate) fn from_natural_size_in_dots(natural_size_in_dots: PhysicalSize<f64>) -> Self { // FIXME: should 'image-resolution' (when implemented) be used *instead* of // `script::dom::htmlimageelement::ImageRequest::current_pixel_density`? // https://drafts.csswg.org/css-images-4/#the-image-resolution let dppx = 1.0; let width = natural_size_in_dots.width as f32 / dppx; let height = natural_size_in_dots.height as f32 / dppx; Self::from_width_and_height(width, height) } pub(crate) fn empty() -> Self { Self { width: None, height: None, ratio: None, } } } #[derive(Debug, MallocSizeOf)] pub(crate) struct CanvasInfo { pub source: Option<ImageKey>, } #[derive(Debug, MallocSizeOf)] pub(crate) struct IFrameInfo { pub pipeline_id: PipelineId, pub browsing_context_id: BrowsingContextId, } #[derive(Debug, MallocSizeOf)] pub(crate) struct ImageInfo { pub image: Option<Image>, pub showing_broken_image_icon: bool, pub url: Option<ServoUrl>, } #[derive(Debug, MallocSizeOf)] pub(crate) struct VideoInfo { pub image_key: Option<ImageKey>, pub poster_url: Option<ServoUrl>, } #[derive(Debug, MallocSizeOf)] pub(crate) enum ReplacedContentKind { Image(ImageInfo), IFrame(IFrameInfo), Canvas(CanvasInfo), Video(VideoInfo), SVGElement { vector_image: Option<VectorImage>, has_viewbox: bool, }, Audio, } impl ReplacedContents { pub fn for_element(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Option<Self> { if let Some(ref data_attribute_string) = node.as_typeless_object_with_data_attribute() && let Some(url) = try_to_parse_image_data_url(data_attribute_string) { return Self::from_image_url(node, context, &ComputedUrl::Valid(ServoArc::new(url))); } let (kind, natural_size) = { if let Some((image_info, natural_size_in_dots)) = node.as_image() { if let Some(content_image) = Self::from_content_property(node, context) { return Some(content_image); } ( ReplacedContentKind::Image(image_info), NaturalSizes::from_natural_size_in_dots(natural_size_in_dots), ) } else if let Some((canvas_info, natural_size_in_dots)) = node.as_canvas() { ( ReplacedContentKind::Canvas(canvas_info), NaturalSizes::from_natural_size_in_dots(natural_size_in_dots), ) } else if let Some(iframe_info) = node.as_iframe() { ( ReplacedContentKind::IFrame(iframe_info), NaturalSizes::empty(), ) } else if let Some((video_info, natural_size_in_dots)) = node.as_video() { ( ReplacedContentKind::Video(video_info), natural_size_in_dots .map_or_else(NaturalSizes::empty, NaturalSizes::from_natural_size_in_dots), ) } else if let Some(svg_data) = node.as_svg() { Self::svg_kind_size(svg_data, context, node) } else if node .as_html_element() .is_some_and(|element| element.local_name() == &local_name!("audio")) { let natural_size = NaturalSizes { width: None, // 40px is the height of the controls. // See /components/script/resources/media-controls.css height: Some(Au::from_px(40)), ratio: None, }; (ReplacedContentKind::Audio, natural_size) } else { return Self::from_content_property(node, context); } }; if let ReplacedContentKind::Image(ImageInfo { image: Some(Image::Raster(ref image)), .. }) = kind { context .image_resolver .handle_animated_image(node.opaque(), image.clone()); } Some(Self { kind, is_content_replacement: false, natural_size, base_fragment_info: node.into(), }) } fn svg_kind_size( svg_data: SVGElementData, context: &LayoutContext, node: ServoLayoutNode<'_>, ) -> (ReplacedContentKind, NaturalSizes) { let rule_cache_conditions = &mut RuleCacheConditions::default(); let mut tree_counting_caches = TreeCountingCaches::default(); let parent_style = node.style(&context.style_context); let style_builder = StyleBuilder::new( context.style_context.stylist.device(), Some(context.style_context.stylist), Some(&parent_style), None, None, false, ); // TODO: use the correct element context in order to properly resolve // `sibling-index()`, like Blink. Or maybe do it like Gecko, and only // accept literals, see https://github.com/w3c/csswg-drafts/issues/14117 let element_context = &DummyElementContext; let to_computed_context = Context::new( style_builder, context.style_context.quirks_mode(), rule_cache_conditions, ContainerSizeQuery::none(), RuleCascadeFlags::empty(), element_context, &mut tree_counting_caches, ); let attr_to_computed = |attr_val: &AttrValue| { if let AttrValue::LengthPercentage(_, length_percentage) = attr_val { length_percentage .to_computed_value(&to_computed_context)? .to_length() } else { None } }; let width = svg_data.width.and_then(attr_to_computed); let height = svg_data.height.and_then(attr_to_computed); let ratio = match (width, height) { (Some(width), Some(height)) if !width.is_zero() && !height.is_zero() => { Some(width.px() / height.px()) }, _ => svg_data.ratio_from_view_box(), }; let natural_size = NaturalSizes { width: width.map(|w| Au::from_f32_px(w.px())), height: height.map(|h| Au::from_f32_px(h.px())), ratio, }; let svg_source = match svg_data.source { None => { // The SVGSVGElement is not yet serialized, so we add it to a list // and hand it over to script to peform the serialization. context .image_resolver .queue_svg_element_for_serialization(node); None }, // If `svg_source_result` is `Err()`, it means that the previous attempt // had errored, then don't attempt to serialize again. Some(svg_source_result) => svg_source_result.ok(), }; let cached_image = svg_source.and_then(|svg_source| { context .image_resolver .get_cached_image_for_url( node.opaque(), svg_source, LayoutImageDestination::BoxTreeConstruction, InternalRequest::Yes, ) .ok() }); let vector_image = cached_image.map(|image| match image { Image::Vector(mut vector_image) => { vector_image.svg_id = Some(svg_data.svg_id); vector_image }, _ => unreachable!("SVG element can't contain a raster image."), }); ( ReplacedContentKind::SVGElement { vector_image, has_viewbox: svg_data.view_box.is_some(), }, natural_size, ) } fn from_content_property(node: ServoLayoutNode<'_>, context: &LayoutContext) -> Option<Self> { // If the `content` property is a single image URL, non-replaced boxes // and images get replaced with the given image. if let Content::Items(GenericContentItems { items, .. }) = node.style(&context.style_context).clone_content() && let [GenericContentItem::Image(image)] = items.as_slice() { // Invalid images are treated as zero-sized. let mut replaced_contents = Self::from_image(node, context, image) .unwrap_or_else(|| Self::zero_sized_invalid_image(node)); replaced_contents.is_content_replacement = true; node.clear_fragments_and_dirty_fragment_caches_of_descendants(); return Some(replaced_contents); } None } pub fn from_image_url( node: ServoLayoutNode<'_>, context: &LayoutContext, image_url: &ComputedUrl, ) -> Option<Self> { let ComputedUrl::Valid(image_url) = image_url else { return None; }; let (image, width, height) = match context.image_resolver.get_or_request_image_or_meta( node.opaque(), image_url.clone().into(), LayoutImageDestination::BoxTreeConstruction, InternalRequest::No, ) { LayoutImageCacheResult::DataAvailable(img_or_meta) => match img_or_meta { ImageOrMetadataAvailable::ImageAvailable { image, .. } => { if let Image::Raster(image) = &image { context .image_resolver .handle_animated_image(node.opaque(), image.clone()); } let metadata = image.metadata(); (Some(image), metadata.width as f32, metadata.height as f32) }, ImageOrMetadataAvailable::MetadataAvailable(metadata, _id) => { (None, metadata.width as f32, metadata.height as f32) }, }, LayoutImageCacheResult::Pending | LayoutImageCacheResult::LoadError => return None, }; Some(Self { kind: ReplacedContentKind::Image(ImageInfo { image, showing_broken_image_icon: false, url: Some(image_url.clone().into()), }), is_content_replacement: false, natural_size: NaturalSizes::from_width_and_height(width, height), base_fragment_info: node.into(), }) } pub fn from_image( element: ServoLayoutNode<'_>, context: &LayoutContext, image: &ComputedImage, ) -> Option<Self> { match image { ComputedImage::Url(image_url) => Self::from_image_url(element, context, image_url), _ => None, // TODO } } pub(crate) fn zero_sized_invalid_image(node: ServoLayoutNode<'_>) -> Self { Self { kind: ReplacedContentKind::Image(ImageInfo { image: None, showing_broken_image_icon: false, url: None, }), is_content_replacement: false, natural_size: NaturalSizes::from_width_and_height(0., 0.), base_fragment_info: node.into(), } } #[inline] fn is_broken_image(&self) -> bool { matches!(&self.kind, ReplacedContentKind::Image(image_info) if image_info.showing_broken_image_icon) } #[inline] fn content_size( &self, axis: Direction, preferred_aspect_ratio: Option<AspectRatio>, get_size_in_opposite_axis: &dyn Fn() -> SizeConstraint, get_fallback_size: &dyn Fn() -> Au, ) -> Au { let Some(ratio) = preferred_aspect_ratio else { return get_fallback_size(); }; let transfer = |size| ratio.compute_dependent_size(axis, size); match get_size_in_opposite_axis() { SizeConstraint::Definite(size) => transfer(size), SizeConstraint::MinMax(min_size, max_size) => get_fallback_size() .clamp_between_extremums(transfer(min_size), max_size.map(transfer)), } } fn calculate_fragment_rect( &self, style: &ServoArc<ComputedValues>, size: PhysicalSize<Au>, ) -> (PhysicalSize<Au>, PhysicalRect<Au>) { if let ReplacedContentKind::Image(ImageInfo { image: Some(Image::Raster(image)), showing_broken_image_icon: true, url: _, }) = &self.kind { let size = Size2D::new( Au::from_f32_px(image.metadata.width as f32), Au::from_f32_px(image.metadata.height as f32), ) .min(size); return (PhysicalSize::zero(), size.into()); } let natural_size = PhysicalSize::new( self.natural_size.width.unwrap_or(size.width), self.natural_size.height.unwrap_or(size.height), ); let object_fit_size = self.natural_size.ratio.map_or(size, |width_over_height| { let preserve_aspect_ratio_with_comparison = |size: PhysicalSize<Au>, comparison: fn(&Au, &Au) -> bool| { let candidate_width = size.height.scale_by(width_over_height); if comparison(&candidate_width, &size.width) { return PhysicalSize::new(candidate_width, size.height); } let candidate_height = size.width.scale_by(1. / width_over_height); debug_assert!(comparison(&candidate_height, &size.height)); PhysicalSize::new(size.width, candidate_height) }; match style.clone_object_fit() { ObjectFit::Fill => size, ObjectFit::Contain => preserve_aspect_ratio_with_comparison(size, PartialOrd::le), ObjectFit::Cover => preserve_aspect_ratio_with_comparison(size, PartialOrd::ge), ObjectFit::None => natural_size, ObjectFit::ScaleDown => { preserve_aspect_ratio_with_comparison(size.min(natural_size), PartialOrd::le) }, } }); let object_position = style.clone_object_position(); let horizontal_position = object_position .horizontal .to_used_value(size.width - object_fit_size.width); let vertical_position = object_position .vertical .to_used_value(size.height - object_fit_size.height); let object_position = PhysicalPoint::new(horizontal_position, vertical_position); ( object_fit_size, PhysicalRect::new(object_position, object_fit_size), ) } pub fn make_fragments( &self, layout_context: &LayoutContext, style: &ServoArc<ComputedValues>, size: PhysicalSize<Au>, ) -> Vec<Fragment> { let (object_fit_size, rect) = self.calculate_fragment_rect(style, size); let clip = PhysicalRect::new(PhysicalPoint::origin(), size); let base = BaseFragment::new(self.base_fragment_info, rect); match &self.kind { ReplacedContentKind::Image(image_info) => image_info .image .as_ref() .and_then(|image| match image { Image::Raster(raster_image) => raster_image.id, Image::Vector(vector_image) => { let scale = layout_context.style_context.device_pixel_ratio(); let width = object_fit_size.width.scale_by(scale.0).to_px(); let height = object_fit_size.height.scale_by(scale.0).to_px(); let size = Size2D::new(width, height); let tag = self.base_fragment_info.tag?; layout_context .image_resolver .rasterize_vector_image( vector_image.id, size, tag.node, vector_image.svg_id, ) .and_then(|i| i.id) }, }) .map(|image_key| { Fragment::Image(Arc::new(ImageFragment { base, style: style.clone().into(), clip, image_key: Some(image_key), showing_broken_image_icon: image_info.showing_broken_image_icon, url: image_info.url.clone(), natural_width: self.natural_size.width, natural_height: self.natural_size.height, })) }) .into_iter() .collect(), ReplacedContentKind::Video(video_info) => { vec![Fragment::Image(Arc::new(ImageFragment { base, style: style.clone().into(), clip, image_key: video_info.image_key, showing_broken_image_icon: false, url: video_info.poster_url.clone(), natural_width: self.natural_size.width, natural_height: self.natural_size.height, }))] }, ReplacedContentKind::IFrame(iframe) => { let size = Size2D::new(rect.size.width.to_f32_px(), rect.size.height.to_f32_px()); let hidpi_scale_factor = layout_context.style_context.device_pixel_ratio(); layout_context.iframe_sizes.lock().insert( iframe.browsing_context_id, IFrameSize { browsing_context_id: iframe.browsing_context_id, pipeline_id: iframe.pipeline_id, viewport_details: ViewportDetails { size, hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), device_size: layout_context.device_size.cast_unit(), }, }, ); vec![Fragment::IFrame(Arc::new(IFrameFragment { base, style: style.clone().into(), pipeline_id: iframe.pipeline_id, }))] }, ReplacedContentKind::Canvas(canvas_info) => { if self.natural_size.width == Some(Au::zero()) || self.natural_size.height == Some(Au::zero()) { return vec![]; } let Some(image_key) = canvas_info.source else { return vec![]; }; vec![Fragment::Image(Arc::new(ImageFragment { base, style: style.clone().into(), clip, image_key: Some(image_key), showing_broken_image_icon: false, url: None, natural_width: self.natural_size.width, natural_height: self.natural_size.height, }))] }, ReplacedContentKind::SVGElement { vector_image, has_viewbox, } => { let Some(vector_image) = vector_image else { return vec![]; }; if !has_viewbox { base.set_rect( PhysicalSize::new( vector_image .metadata .width .try_into() .map_or(MAX_AU, Au::from_px), vector_image .metadata .height .try_into() .map_or(MAX_AU, Au::from_px), ) .into(), ); } let scale = layout_context.style_context.device_pixel_ratio(); let content_size = base.rect().size; let raster_size = Size2D::new( content_size.width.scale_by(scale.0).to_px(), content_size.height.scale_by(scale.0).to_px(), ); let tag = self.base_fragment_info.tag.unwrap(); layout_context .image_resolver .rasterize_vector_image( vector_image.id, raster_size, tag.node, vector_image.svg_id, ) .and_then(|image| image.id) .map(|image_key| { Fragment::Image(Arc::new(ImageFragment { base, style: style.clone().into(), clip, image_key: Some(image_key), showing_broken_image_icon: false, url: None, natural_width: self.natural_size.width, natural_height: self.natural_size.height, })) }) .into_iter() .collect() }, ReplacedContentKind::Audio => vec![], } } pub(crate) fn preferred_aspect_ratio( &self, style: &ComputedValues, padding_border_sums: &LogicalVec2<Au>, ) -> Option<AspectRatio> { if matches!(self.kind, ReplacedContentKind::Audio) { // This isn't specified, but other browsers don't support `aspect-ratio` on `<audio>`. // See <https://phabricator.services.mozilla.com/D118245> return None; } if self.is_broken_image() { // This isn't specified, but when an image is broken, we should prefer to the aspect // ratio from the style, rather than the aspect ratio from the broken image icon. // Note that the broken image icon *does* affect the content size of the image // though as we want the image to be as big as the icon if the size was not specified // in the style. style.preferred_aspect_ratio(None, padding_border_sums) } else { style.preferred_aspect_ratio(self.natural_size.ratio, padding_border_sums) } } /// The inline size that would result from combining the natural size /// and the default object size, but disregarding the specified size. /// <https://drafts.csswg.org/css-images-3/#natural-dimensions> /// <https://drafts.csswg.org/css-images-3/#default-object-size> /// <https://drafts.csswg.org/css-images-3/#specified-size> pub(crate) fn fallback_inline_size(&self, writing_mode: WritingMode) -> Au { if writing_mode.is_horizontal() { self.natural_size.width.unwrap_or_else(|| Au::from_px(300)) } else { self.natural_size.height.unwrap_or_else(|| Au::from_px(150)) } } /// The block size that would result from combining the natural size /// and the default object size, but disregarding the specified size. /// <https://drafts.csswg.org/css-images-3/#natural-dimensions> /// <https://drafts.csswg.org/css-images-3/#default-object-size> /// <https://drafts.csswg.org/css-images-3/#specified-size> pub(crate) fn fallback_block_size(&self, writing_mode: WritingMode) -> Au { if writing_mode.is_horizontal() { self.natural_size.height.unwrap_or_else(|| Au::from_px(150)) } else { self.natural_size.width.unwrap_or_else(|| Au::from_px(300)) } } pub(crate) fn logical_natural_sizes( &self, writing_mode: WritingMode, ) -> LogicalVec2<Option<Au>> { if writing_mode.is_horizontal() { LogicalVec2 { inline: self.natural_size.width, block: self.natural_size.height, } } else { LogicalVec2 { inline: self.natural_size.height, block: self.natural_size.width, } } } #[inline] pub(crate) fn layout_style<'a>(&self, base: &'a LayoutBoxBase) -> LayoutStyle<'a> { LayoutStyle::Default(&base.style) } pub(crate) fn layout( &self, layout_context: &LayoutContext, containing_block_for_children: &ContainingBlock, preferred_aspect_ratio: Option<AspectRatio>, base: &LayoutBoxBase, lazy_block_size: &LazySize, ) -> IndependentFormattingContextLayoutResult { let writing_mode = base.style.writing_mode; let inline_size = containing_block_for_children.size.inline; let content_block_size = self.content_size( Direction::Block, preferred_aspect_ratio, &|| SizeConstraint::Definite(inline_size), &|| self.fallback_block_size(writing_mode), ); let size = LogicalVec2 { inline: inline_size, block: lazy_block_size.resolve(|| content_block_size), } .to_physical_size(writing_mode); IndependentFormattingContextLayoutResult { baselines: Default::default(), collapsible_margins_in_children: CollapsedBlockMargins::zero(), content_block_size, content_inline_size_for_table: None, // The result doesn't depend on `containing_block_for_children.size.block`, // but it depends on `lazy_block_size`, which is probably tied to that. depends_on_block_constraints: true, fragments: self.make_fragments(layout_context, &base.style, size), specific_layout_info: None, } } } impl ComputeInlineContentSizes for ReplacedContents { fn compute_inline_content_sizes( &self, _: &LayoutContext, constraint_space: &ConstraintSpace, ) -> InlineContentSizesResult { let inline_content_size = self.content_size( Direction::Inline, constraint_space.preferred_aspect_ratio, &|| constraint_space.block_size, &|| self.fallback_inline_size(constraint_space.style.writing_mode), ); InlineContentSizesResult { sizes: inline_content_size.into(), depends_on_block_constraints: constraint_space.preferred_aspect_ratio.is_some(), } } } fn try_to_parse_image_data_url(string: &str) -> Option<Url> { if !string.starts_with("data:") { return None; } let data_url = DataUrl::process(string).ok()?; let mime_type = data_url.mime_type(); if mime_type.type_ != "image" { return None; } // TODO: Find a better way to test for supported image formats. Currently this type of check is // repeated several places in Servo, but should be centralized somehow. if !matches!( mime_type.subtype.as_str(), "png" | "jpeg" | "gif" | "webp" | "bmp" | "ico" ) { return None; } Url::parse(string).ok() }