/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/layout/display_list/mod.rs
2 591 строка
103 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::cell::{OnceCell, RefCell}; use std::sync::Arc; use app_units::{AU_PER_PX, Au}; use clip::Clip; pub(crate) use clip::ClipId; use euclid::{Box2D, Point2D, Rect, Scale, SideOffsets2D, Size2D, UnknownUnit, Vector2D}; use fonts::ShapedTextSlice; use gradient::WebRenderGradient; use layout_api::ReflowStatistics; use net_traits::image_cache::Image as CachedImage; use paint_api::display_list::{PaintDisplayListInfo, SpatialTreeNodeInfo}; use servo_arc::Arc as ServoArc; use servo_base::id::{PipelineId, ScrollTreeNodeId}; use servo_base::text::Utf32CodeUnits; use servo_config::opts::{DiagnosticsLogging, DiagnosticsLoggingOption}; use servo_config::{pref, prefs}; use servo_url::ServoUrl; use style::Zero; use style::color::{AbsoluteColor, ColorSpace}; use style::computed_values::background_blend_mode::SingleComputedValue as BackgroundBlendMode; use style::computed_values::border_image_outset::T as BorderImageOutset; use style::computed_values::mix_blend_mode::T as ComputedMixBlendMode; use style::computed_values::overflow_x::T as ComputedOverflow; use style::computed_values::text_decoration_style::{ T as ComputedTextDecorationStyle, T as TextDecorationStyle, }; use style::computed_values::text_decoration_thickness::T as TextDecorationThickness; use style::dom::OpaqueNode; use style::properties::ComputedValues; use style::properties::longhands::visibility::computed_value::T as Visibility; use style::properties::style_structs::Border; use style::values::computed::basic_shape::ClipPath as ComputedClipPath; use style::values::computed::{ BorderImageSideWidth, BorderImageWidth, BorderStyle, LengthPercentage, NonNegativeLengthOrNumber, NumberOrPercentage, OutlineStyle, }; use style::values::generics::NonNegative; use style::values::generics::color::ColorOrAuto; use style::values::generics::rect::Rect as StyleRect; use style::values::specified::text::TextDecorationLine; use style_traits::{CSSPixel as StyloCSSPixel, DevicePixel as StyloDevicePixel}; use webrender_api::units::{ DeviceIntSize, DevicePixel, LayoutPixel, LayoutPoint, LayoutRect, LayoutSideOffsets, LayoutSize, }; use webrender_api::{ self as wr, BorderDetails, BorderRadius, BorderSide, BoxShadowClipMode, BuiltDisplayList, ClipChainId, ClipMode, ColorF, CommonItemProperties, ComplexClipRegion, GlyphInstance, MixBlendMode, NinePatchBorder, NinePatchBorderSource, NormalBorder, PrimitiveFlags, PropertyBinding, PropertyBindingKey, RasterSpace, SpatialId, StackingContextFlags, TransformStyle, units, }; use wr::units::LayoutVector2D; use crate::context::{ImageResolver, ResolvedImage}; use crate::display_list::background::BackgroundPainter; use crate::display_list::conversions::FilterToWebRender; pub(crate) use crate::display_list::conversions::ToWebRender; use crate::display_list::paint_traversal::{PaintTraversal, PaintTraversalHandler, TraversalState}; use crate::fragment_tree::{ BackgroundMode, BaseFragment, BoxFragment, BoxFragmentWithStyle, ContainingBlockCalculation, Fragment, FragmentFlags, FragmentStatus, FragmentTree, IFrameFragment, ImageFragment, PositioningFragment, SpecificLayoutInfo, Tag, TextFragment, }; use crate::geom::{ LengthPercentageOrAuto, PhysicalPoint, PhysicalRect, PhysicalSides, PhysicalSize, }; use crate::replaced::NaturalSizes; use crate::style_ext::{BorderStyleColor, ComputedValuesExt}; mod background; mod clip; mod conversions; mod gradient; mod hit_test; mod paint_timing_handler; mod paint_traversal; mod stacking_context; pub(crate) use hit_test::HitTest; pub(crate) use paint_timing_handler::PaintTimingHandler; pub(crate) use stacking_context::*; const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(AU_PER_PX); pub(crate) struct DisplayListBuilder<'a> { /// The [`FragmentTree`] that we are building a display list for. fragment_tree: &'a FragmentTree, /// The current [`ScrollTreeNodeId`] for this [`DisplayListBuilder`]. This is /// necessary because some pieces of fragments as backgrounds with /// `background-attachment: fixed` need to not scroll while the rest of the fragment /// does. current_reference_frame_scroll_node_id: ScrollTreeNodeId, /// The [`wr::DisplayListBuilder`] for this Servo [`DisplayListBuilder`]. pub webrender_display_list_builder: &'a mut wr::DisplayListBuilder, /// The [`PaintDisplayListInfo`] used to collect display list items and metadata. pub paint_info: &'a mut PaintDisplayListInfo, /// Data about the fragments that are highlighted by the inspector, if any. /// /// This data is collected during the traversal of the fragment tree and used /// to paint the highlight at the very end. inspector_highlight: Option<InspectorHighlight>, /// Whether or not the `<body>` element should be painted. This is false if the root `<html>` /// element inherits the `<body>`'s background to paint the page canvas background. /// See <https://drafts.csswg.org/css-backgrounds/#body-background>. paint_body_background: bool, /// A mapping from [`ClipId`] To WebRender [`ClipChainId`] used when building this WebRender /// display list. clip_map: Vec<ClipChainId>, /// An [`ImageResolver`] to use during display list construction. image_resolver: Arc<ImageResolver>, /// The device pixel ratio used for this `Document`'s display list. device_pixel_ratio: Scale<f32, StyloCSSPixel, StyloDevicePixel>, /// Handler for all Paint Timings paint_timing_handler: &'a mut PaintTimingHandler, /// Statistics collected about the reflow, in order to write tests for incremental layout. reflow_statistics: &'a mut ReflowStatistics, } struct InspectorHighlight { /// The node that should be highlighted tag: Tag, /// Accumulates information about the fragments that belong to the highlighted node. /// /// This information is collected as the fragment tree is traversed to build the /// display list. state: Option<HighlightTraversalState>, } struct HighlightTraversalState { /// The smallest rectangle that fully encloses all fragments created by the highlighted /// dom node, if any. content_box: Rect<Au, StyloCSSPixel>, spatial_id: SpatialId, clip_chain_id: ClipChainId, /// When the highlighted fragment is a box fragment we remember the information /// needed to paint padding, border and margin areas. maybe_box_fragment: Option<Arc<BoxFragment>>, } impl InspectorHighlight { fn for_node(node: OpaqueNode) -> Self { Self { tag: Tag { node, // TODO: Support highlighting pseudo-elements. pseudo_element_chain: Default::default(), }, state: None, } } } impl DisplayListBuilder<'_> { #[expect(clippy::too_many_arguments)] pub(crate) fn build( stacking_context_tree: &mut StackingContextTree, fragment_tree: &FragmentTree, image_resolver: Arc<ImageResolver>, device_pixel_ratio: Scale<f32, StyloCSSPixel, StyloDevicePixel>, highlighted_dom_node: Option<OpaqueNode>, debug: &DiagnosticsLogging, paint_timing_handler: &mut PaintTimingHandler, reflow_statistics: &mut ReflowStatistics, ) -> BuiltDisplayList { // Build the rest of the display list which inclues all of the WebRender primitives. let paint_info = &mut stacking_context_tree.paint_info; let pipeline_id = paint_info.pipeline_id; let mut webrender_display_list_builder = webrender_api::DisplayListBuilder::new(pipeline_id); webrender_display_list_builder.begin(); // `dump_serialized_display_list` doesn't actually print anything. It sets up // the display list for printing the serialized version when `finalize()` is called. // We need to call this before adding any display items so that they are printed // during `finalize()`. if debug.is_enabled(DiagnosticsLoggingOption::DisplayList) { webrender_display_list_builder.dump_serialized_display_list(); } let _span = profile_traits::trace_span!("DisplayListBuilder::build").entered(); let mut builder = DisplayListBuilder { fragment_tree, current_reference_frame_scroll_node_id: paint_info.root_reference_frame_id, webrender_display_list_builder: &mut webrender_display_list_builder, paint_info, inspector_highlight: highlighted_dom_node.map(InspectorHighlight::for_node), paint_body_background: true, clip_map: Default::default(), image_resolver, device_pixel_ratio, paint_timing_handler, reflow_statistics, }; // Clear any caret color from previous display list constructions. builder.paint_info.caret_property_binding = None; builder.add_all_spatial_nodes(); for clip in stacking_context_tree.clip_store.0.iter() { builder.add_clip_to_display_list(clip); } // Add a single hit test that covers the entire viewport, so that WebRender knows // which pipeline it hits when doing hit testing. let pipeline_id = builder.paint_info.pipeline_id; let viewport_size = builder.paint_info.viewport_details.size; let viewport_rect = LayoutRect::from_size(viewport_size.cast_unit()); builder.wr().push_hit_test( viewport_rect, ClipChainId::INVALID, SpatialId::root_reference_frame(pipeline_id), PrimitiveFlags::default(), (0, 0), /* tag */ ); PaintTraversal::traverse(&stacking_context_tree.root_stacking_context, &mut builder); builder.paint_dom_inspector_highlight(); webrender_display_list_builder.end().1 } fn wr(&mut self) -> &mut wr::DisplayListBuilder { self.webrender_display_list_builder } fn pipeline_id(&self) -> wr::PipelineId { self.paint_info.pipeline_id } fn mark_is_paintable(&mut self) { self.paint_info.is_paintable = true; } fn mark_is_contentful(&mut self) { self.paint_info.is_contentful = true; } fn spatial_id(&self, id: ScrollTreeNodeId) -> SpatialId { self.paint_info.scroll_tree.webrender_id(id) } fn clip_chain_id(&self, id: ClipId) -> ClipChainId { match id { ClipId::INVALID => ClipChainId::INVALID, _ => *self .clip_map .get(id.0) .expect("Should never try to get clip before adding it to WebRender display list"), } } pub(crate) fn add_all_spatial_nodes(&mut self) { // A count of the number of SpatialTree nodes pushed to the WebRender display // list. This is merely to ensure that the currently-unused SpatialTreeItemKey // produced for every SpatialTree node is unique. let mut scroll_tree = std::mem::take(&mut self.paint_info.scroll_tree); let mut mapping = Vec::with_capacity(scroll_tree.nodes.len()); mapping.push(SpatialId::root_reference_frame(self.pipeline_id())); mapping.push(SpatialId::root_scroll_node(self.pipeline_id())); for node in scroll_tree.nodes.iter().skip(2) { let parent_scroll_node_id = node .parent .expect("Should have already added root reference frame"); let parent_spatial_node_id = mapping .get(parent_scroll_node_id.index) .expect("Should add spatial nodes to display list in order"); mapping.push(match &node.info { SpatialTreeNodeInfo::ReferenceFrame(info) => { let spatial_id = self.wr().push_reference_frame( info.origin, *parent_spatial_node_id, info.transform_style, PropertyBinding::Value(*info.transform.to_transform()), info.kind, ); self.wr().pop_reference_frame(); spatial_id }, SpatialTreeNodeInfo::Scroll(info) => { self.wr().define_scroll_frame( *parent_spatial_node_id, info.external_id, info.content_rect, info.clip_rect, LayoutVector2D::zero(), /* external_scroll_offset */ 0, /* scroll_offset_generation */ wr::HasScrollLinkedEffect::No, ) }, SpatialTreeNodeInfo::Sticky(info) => { self.wr().define_sticky_frame( *parent_spatial_node_id, info.frame_rect, info.margins, info.vertical_offset_bounds, info.horizontal_offset_bounds, LayoutVector2D::zero(), /* previously_applied_offset */ None, /* transform */ ) }, }); } scroll_tree.update_mapping(mapping); self.paint_info.scroll_tree = scroll_tree; } /// Add the given [`Clip`] to the WebRender display list and create a mapping from /// its [`ClipId`] to a WebRender [`ClipChainId`]. This happens: /// - When WebRender display list construction starts: All clips created during the /// `StackingContextTree` construction are added in one batch. These clips are used /// for things such as `overflow: scroll` elements. /// - When a clip is added during WebRender display list construction for individual /// items. In that case, this is called by [`Self::maybe_create_clip`]. pub(crate) fn add_clip_to_display_list(&mut self, clip: &Clip) -> ClipChainId { assert_eq!( clip.id.0, self.clip_map.len(), "Clips should be added in order" ); let spatial_id = self.spatial_id(clip.parent_scroll_node_id); let new_clip_id = if clip.radii.is_zero() { self.wr().define_clip_rect(spatial_id, clip.rect) } else { self.wr().define_clip_rounded_rect( spatial_id, ComplexClipRegion { rect: clip.rect, radii: clip.radii, mode: ClipMode::Clip, }, ) }; // WebRender has two different ways of expressing "no clip." ClipChainId::INVALID should be // used for primitives, but `None` is used for stacking contexts and clip chains. We convert // to the `Option<ClipChainId>` representation here. Just passing Some(ClipChainId::INVALID) // leads to a crash. let parent_clip_chain_id = match self.clip_chain_id(clip.parent_clip_id) { ClipChainId::INVALID => None, parent => Some(parent), }; let clip_chain_id = self .wr() .define_clip_chain(parent_clip_chain_id, [new_clip_id]); self.clip_map.push(clip_chain_id); clip_chain_id } /// Add a new clip to the WebRender display list being built. This only happens during /// WebRender display list building and these clips should be added after all clips /// from the `StackingContextTree` have already been processed. fn maybe_create_clip( &mut self, state: &TraversalState, radii: wr::BorderRadius, rect: units::LayoutRect, force_clip_creation: bool, ) -> Option<ClipChainId> { if radii.is_zero() && !force_clip_creation { return None; } Some(self.add_clip_to_display_list(&Clip { id: ClipId(self.clip_map.len()), radii, rect, parent_scroll_node_id: state.spatial_id, parent_clip_id: state.clip_id, })) } fn push_webrender_stacking_context_if_necessary( &mut self, stacking_context: &StackingContext, ) -> bool { if stacking_context.context_type == StackingContextType::StackingContainer { return false; } let mut is_blend_container = stacking_context.children.iter().any(|child| { child.fragment().is_some_and(|fragment| { fragment.style().clone_mix_blend_mode() != ComputedMixBlendMode::Normal }) }); let primitive_flags; let transform_style; let mix_blend_mode; let mut filters: Vec<_>; let mut stacking_context_flags = StackingContextFlags::empty(); match &stacking_context.fragment { StackingContextFragments::Fragment(fragment) => { let style = fragment.style(); let effects = style.get_effects(); transform_style = style .used_transform_style(fragment.base.flags) .to_webrender(); mix_blend_mode = effects.mix_blend_mode.to_webrender(); primitive_flags = style.get_webrender_primitive_flags(); // Do not create another blend container stacking context started by the root // element, because the root background is painted above of it (at the root // stacking context, which sits above the root fragment). // // TODO: Would it be cleaner to paint the root background at the root fragment // instead of the root stacking context? is_blend_container &= !fragment.base.flags.contains(FragmentFlags::IS_ROOT_ELEMENT); // WebRender only uses the stacking context to apply certain effects. If we don't // actually need to create a stacking context, just avoid creating one. if !is_blend_container && effects.filter.0.is_empty() && effects.opacity == 1.0 && effects.mix_blend_mode == ComputedMixBlendMode::Normal && !style.has_effective_transform_or_perspective(FragmentFlags::empty()) && style.get_svg().clip_path == ComputedClipPath::None && transform_style == TransformStyle::Flat { return false; } // Create the filter pipeline. let current_color = &style.get_inherited_text().color; filters = effects .filter .0 .iter() .map(|filter| FilterToWebRender::to_webrender(filter, current_color)) .collect(); if effects.opacity != 1.0 { filters.push(wr::FilterOp::Opacity( effects.opacity.into(), effects.opacity, )); } }, // WebRender only needs a stacking context at the root when the root stacking // context itself is a blend container. StackingContextFragments::Root if is_blend_container => { transform_style = TransformStyle::Flat; primitive_flags = PrimitiveFlags::empty(); mix_blend_mode = MixBlendMode::Normal; filters = Vec::new(); }, _ => return false, }; if is_blend_container { stacking_context_flags.insert(StackingContextFlags::IS_BLEND_CONTAINER); } // WebRender has two different ways of expressing "no clip." ClipChainId::INVALID // should be used for primitives, but `None` is used for stacking contexts and // clip chains. We convert to the `Option<ClipChainId>` representation here. Just // passing Some(ClipChainId::INVALID) causes a panic. let clip_chain_id = match stacking_context.clip_id { ClipId::INVALID => None, clip_id => Some(self.clip_chain_id(clip_id)), }; let spatial_id = self.spatial_id(stacking_context.scroll_tree_node_id); self.wr().push_stacking_context( spatial_id, primitive_flags, clip_chain_id, transform_style, mix_blend_mode, &filters, &[], // filter_datas wr::RasterSpace::Screen, stacking_context_flags, None, // snapshot ); true } fn common_properties( &self, state: &TraversalState, clip_rect: units::LayoutRect, style: &ComputedValues, ) -> wr::CommonItemProperties { // TODO(mrobinson): We should take advantage of this field to pass hit testing // information. This will allow us to avoid creating hit testing display items // for fragments that paint their entire border rectangle. wr::CommonItemProperties { clip_rect, spatial_id: self.spatial_id(state.spatial_id), clip_chain_id: self.clip_chain_id(state.clip_id), flags: style.get_webrender_primitive_flags(), } } /// Draw highlights around the node that is currently hovered in the devtools. fn paint_dom_inspector_highlight(&mut self) { let Some(highlight) = self .inspector_highlight .take() .and_then(|highlight| highlight.state) else { return; }; const CONTENT_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { r: 0.23, g: 0.7, b: 0.87, a: 0.5, }; const PADDING_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { r: 0.49, g: 0.3, b: 0.7, a: 0.5, }; const BORDER_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { r: 0.2, g: 0.2, b: 0.2, a: 0.5, }; const MARGIN_BOX_HIGHLIGHT_COLOR: webrender_api::ColorF = webrender_api::ColorF { r: 1., g: 0.93, b: 0., a: 0.5, }; // Highlight content box let content_box = highlight.content_box.to_webrender(); let properties = wr::CommonItemProperties { clip_rect: content_box, spatial_id: highlight.spatial_id, clip_chain_id: highlight.clip_chain_id, flags: wr::PrimitiveFlags::default(), }; self.wr() .push_rect(&properties, content_box, CONTENT_BOX_HIGHLIGHT_COLOR); // Highlight margin, border and padding if let Some(box_fragment) = highlight.maybe_box_fragment { let mut paint_highlight = |color: webrender_api::ColorF, fragment_relative_bounds: PhysicalRect<Au>, widths: webrender_api::units::LayoutSideOffsets| { if widths.is_zero() { return; } let bounds = box_fragment .offset_by_containing_block( &fragment_relative_bounds, ContainingBlockCalculation::AlreadyDoneWithStackingContextTree, ) .to_webrender(); // We paint each highlighted area as if it was a border for simplicity let border_style = wr::BorderSide { color, style: wr::BorderStyle::Solid, }; let details = wr::BorderDetails::Normal(wr::NormalBorder { top: border_style, right: border_style, bottom: border_style, left: border_style, radius: webrender_api::BorderRadius::default(), do_aa: true, }); let common = wr::CommonItemProperties { clip_rect: bounds, spatial_id: highlight.spatial_id, clip_chain_id: highlight.clip_chain_id, flags: wr::PrimitiveFlags::default(), }; self.wr().push_border(&common, bounds, widths, details) }; paint_highlight( PADDING_BOX_HIGHLIGHT_COLOR, box_fragment.padding_rect(), box_fragment.padding.to_webrender(), ); paint_highlight( BORDER_BOX_HIGHLIGHT_COLOR, box_fragment.border_rect(), box_fragment.border.to_webrender(), ); paint_highlight( MARGIN_BOX_HIGHLIGHT_COLOR, box_fragment.margin_rect(), box_fragment.margin.to_webrender(), ); } } fn check_if_paintable(&mut self, bounds: LayoutRect, clip_rect: LayoutRect, opacity: f32) { // From <https://www.w3.org/TR/paint-timing/#paintable>: // An element el is paintable when all of the following apply: // > el is being rendered. // > el’s used visibility is visible. // Above conditions are met, as we selectively call this API. // > el and all of its ancestors' used opacity is greater than zero. if opacity <= 0.0 { return; } // > el’s paintable bounding rect intersects with the scrolling area of the document. if self .paint_timing_handler .check_bounding_rect(bounds, clip_rect) { self.mark_is_paintable(); } } #[allow(clippy::too_many_arguments)] fn check_for_lcp_candidate( &mut self, state: &TraversalState, bounds: LayoutRect, clip_rect: LayoutRect, tag: Option<Tag>, url: Option<ServoUrl>, natural_width: Option<Au>, natural_height: Option<Au>, ) { if !pref!(largest_contentful_paint_enabled) { return; } let transform = self .paint_info .scroll_tree .cumulative_node_to_root_transform(state.spatial_id); self.paint_timing_handler.compute_new_lcp_candidate( tag, bounds, clip_rect, transform, url, natural_width, natural_height, ); } fn visit_stacking_context_reference_frame_info( &mut self, stacking_context: &StackingContext, ) -> (usize, Option<ScrollTreeNodeId>) { let Some(reference_frame_info) = &stacking_context.reference_frame_info else { return (0, None); }; // Note: Reference frames always establish a stacking context, so it is fine to check if // this stacking context establishes a reference frame as well. We don't need to check // every fragment. let old_reference_frame_spatial_id = std::mem::replace( &mut self.current_reference_frame_scroll_node_id, stacking_context.scroll_tree_node_id, ); if reference_frame_info.captured_clip_id == ClipId::INVALID { return (0, Some(old_reference_frame_spatial_id)); } // Although there is nothing in the display list API that prevents it, WebRender // expects that reference frames that alter the coordinate space of their contents do // not propagate clips to those contents. In order to achieve this, push an extra // stacking context here that only specifies a clip. This stacking context will contain // all of the contents of the reference frame, but because it is added in the parent // spatial node, it has the coordinate space of the reference frame parent. In addition // to this, reference frames reset the base `ClipId` value for descendants to // `ClipId::INVALID` during stacking context tree construction. let clip_chain_id = Some(self.clip_chain_id(reference_frame_info.captured_clip_id)); let spatial_id = self.spatial_id(reference_frame_info.parent_spatial_node_id); self.wr().push_stacking_context( spatial_id, PrimitiveFlags::default(), clip_chain_id, webrender_api::TransformStyle::Flat, webrender_api::MixBlendMode::Normal, &[], // filters, &[], // filter_datas wr::RasterSpace::Screen, wr::StackingContextFlags::empty(), None, // snapshot ); (1, Some(old_reference_frame_spatial_id)) } } impl PaintTraversalHandler for DisplayListBuilder<'_> { /// A tuple composed of the number of real WebRender stacking contexts pushed /// and the previous `Self::current_reference_frame_scroll_node_id` value of /// the `DisplayListBuilder` when a stacking context was visited (or `None` if /// the value was unmodified). type StackingContextState = (usize, Option<ScrollTreeNodeId>); fn visit_stacking_context( &mut self, stacking_context: &StackingContext, ) -> Self::StackingContextState { let (mut stacking_contexts_pushed, old_reference_frame) = self.visit_stacking_context_reference_frame_info(stacking_context); if self.push_webrender_stacking_context_if_necessary(stacking_context) { stacking_contexts_pushed += 1; } (stacking_contexts_pushed, old_reference_frame) } fn leave_stacking_context( &mut self, _: &TraversalState, stacking_context_state: Self::StackingContextState, ) { let (stacking_contexts_pushed, old_reference_frame) = stacking_context_state; for _ in 0..stacking_contexts_pushed { self.wr().pop_stacking_context(); } if let Some(old_reference_frame) = old_reference_frame { self.current_reference_frame_scroll_node_id = old_reference_frame; } } fn visit_box(&mut self, state: &TraversalState, fragment: &BoxFragmentWithStyle<'_>) { fragment.base.visit_fragment(self); if let Some(mut inspector_highlight) = self.inspector_highlight.take() && fragment.base.tag == Some(inspector_highlight.tag) { inspector_highlight.register_fragment_of_highlighted_dom_node(self, state, fragment); self.inspector_highlight = Some(inspector_highlight); } if fragment.style().get_inherited_box().visibility != Visibility::Visible { return; }; BuilderForBoxFragment::new(fragment, state.origin).build(self, state) } fn visit_iframe(&mut self, state: &TraversalState, fragment: &Arc<IFrameFragment>) { fragment.base.visit_fragment(self); let style = fragment.style.borrow(); if style.get_inherited_box().visibility != Visibility::Visible { return; } let rect = fragment.base.rect().translate(state.origin.to_vector()); let common = self.common_properties(state, rect.to_webrender(), &style); self.wr().push_iframe( rect.to_webrender(), common.clip_rect, &wr::SpaceAndClipInfo { spatial_id: common.spatial_id, clip_chain_id: common.clip_chain_id, }, fragment.pipeline_id.into(), true, ); // From <https://www.w3.org/TR/paint-timing/#mark-paint-timing>: // > A parent frame should not be aware of the paint events from its child iframes, and // > vice versa. This means that a frame that contains just iframes will have first paint // > (due to the enclosing boxes of the iframes) but no first contentful paint. self.check_if_paintable(rect.to_webrender(), common.clip_rect, style.clone_opacity()); } fn visit_image( &mut self, state: &TraversalState, containing_block: PhysicalRect<Au>, fragment: &Arc<ImageFragment>, ) { fragment.base.visit_fragment(self); let style = fragment.style.borrow(); if style.get_inherited_box().visibility != Visibility::Visible { return; } let image_rendering = style.get_inherited_box().image_rendering.to_webrender(); let rect = fragment .base .rect() .translate(containing_block.origin.to_vector()) .to_webrender(); let clip = fragment .clip .translate(containing_block.origin.to_vector()) .to_webrender(); let common = self.common_properties(state, clip, &style); if let Some(image_key) = fragment.image_key { self.wr().push_image( &common, rect, image_rendering, wr::AlphaType::PremultipliedAlpha, image_key, wr::ColorF::WHITE, ); self.check_if_paintable(rect, common.clip_rect, style.clone_opacity()); // From <https://www.w3.org/TR/paint-timing/#contentful>: // An element target is contentful when one or more of the following apply: // > target is a replaced element representing an available image. // From: <https://html.spec.whatwg.org/multipage/#img-available> // When an image request's state is either partially available or completely available, // the image request is said to be available. // Hence, Skip Broken Images. if !fragment.showing_broken_image_icon { self.mark_is_contentful(); self.check_for_lcp_candidate( state, rect, common.clip_rect, fragment.base.tag, fragment.url.clone(), fragment.natural_width, fragment.natural_height, ); } } if fragment.showing_broken_image_icon { Fragment::build_display_list_for_broken_image_border(self, &containing_block, &common); } } fn visit_text( &mut self, state: &TraversalState, containing_block: PhysicalRect<Au>, fragment: &Arc<TextFragment>, ) { fragment.base.visit_fragment(self); let style = fragment.style(); if style.get_inherited_box().visibility != Visibility::Visible { return; } Fragment::build_display_list_for_text_fragment(fragment, self, state, &containing_block); } fn visit_positioning(&mut self, _state: &TraversalState, fragment: &Arc<PositioningFragment>) { fragment.base.visit_fragment(self); } /// This is an implementation of step 3 from: /// <https://drafts.csswg.org/css-position-4/#paint-a-stacking-context>: /// /// - See also: <https://drafts.csswg.org/css-backgrounds/#special-backgrounds>. /// - Note: This is only called for the root `StackingContext`. fn visit_box_for_root_background(&mut self, state: &TraversalState) { let Some(fragment) = self.fragment_tree.root_box_fragment() else { return; }; let fragment = fragment.with_style(); let source_style = { // > For documents whose root element is an HTML HTML element or an XHTML html element // > [HTML]: if the computed value of background-image on the root element is none and its // > background-color is transparent, user agents must instead propagate the computed // > values of the background properties from that element’s first HTML BODY or XHTML body // > child element. let root_fragment_style = fragment.style(); if root_fragment_style.background_is_transparent() { let body_fragment = self.fragment_tree.body_fragment(); self.paint_body_background = body_fragment.is_none(); body_fragment .map(|body_fragment| body_fragment.style().clone()) .unwrap_or(fragment.style().clone()) } else { root_fragment_style.clone() } }; // This can happen if the root fragment does not have a `<body>` child (either because it is // `display: none` or `display: contents`) or if the `<body>`'s background is transparent. if source_style.background_is_transparent() { return; } // The painting area is theoretically the infinite 2D plane, // but we need a rectangle with finite coordinates. // // If the document is smaller than the viewport (and doesn’t scroll), // we still want to paint the rest of the viewport. // If it’s larger, we also want to paint areas reachable after scrolling. let painting_area = self .fragment_tree .initial_containing_block .union(&self.fragment_tree.scrollable_overflow()) .to_webrender(); let background_color = source_style.resolve_color(&source_style.get_background().background_color); if background_color.alpha > 0.0 { let common = self.common_properties(state, painting_area, &source_style); let color = rgba(background_color); self.wr().push_rect(&common, painting_area, color); // From <https://www.w3.org/TR/paint-timing/#sec-terminology>: // First paint ... includes non-default background paint and the enclosing box of an iframe. // The spec is vague. See also: https://github.com/w3c/paint-timing/issues/122 let default_background_color = servo_config::pref!(shell_background_color_rgba); let default_background_color = AbsoluteColor::new( ColorSpace::Srgb, default_background_color[0] as f32, default_background_color[1] as f32, default_background_color[2] as f32, default_background_color[3] as f32, ) .into_srgb_legacy(); if background_color != default_background_color { self.mark_is_paintable(); } } let fragment_builder = BuilderForBoxFragment::new( &fragment, self.fragment_tree.initial_containing_block.origin, ); let painter = BackgroundPainter { style: &source_style, painting_area_override: Some(painting_area), positioning_area_override: None, }; fragment_builder.build_background_image(self, state, &painter); } fn visit_box_for_outline(&mut self, state: &TraversalState, fragment: &Arc<BoxFragment>) { let fragment = fragment.with_style(); if fragment.style().get_inherited_box().visibility != Visibility::Visible { return; }; BuilderForBoxFragment::new(&fragment, state.origin).build_outline(self, state) } fn visit_box_for_collapsed_table_borders( &mut self, state: &TraversalState, fragment: &BoxFragmentWithStyle<'_>, ) { if fragment.style().get_inherited_box().visibility != Visibility::Visible { return; }; BuilderForBoxFragment::new(fragment, state.origin) .build_collapsed_table_borders(self, state) } } impl InspectorHighlight { fn register_fragment_of_highlighted_dom_node( &mut self, builder: &DisplayListBuilder, traversal_state: &TraversalState, fragment: &Arc<BoxFragment>, ) { let spatial_id = builder.spatial_id(traversal_state.spatial_id); let clip_chain_id = builder.clip_chain_id(traversal_state.clip_id); let state = self.state.get_or_insert_with(|| HighlightTraversalState { content_box: Rect::zero(), spatial_id, clip_chain_id, maybe_box_fragment: Some(fragment.clone()), }); // We only need to highlight the first `SpatialId`. Typically this will include the bottommost // fragment for a node, which generally surrounds the entire content. if spatial_id != state.spatial_id { return; } if clip_chain_id != ClipChainId::INVALID && state.clip_chain_id != ClipChainId::INVALID { debug_assert_eq!( clip_chain_id, state.clip_chain_id, "Fragments of the same node must either have no clip chain or the same one" ); } state.maybe_box_fragment = Some(fragment.clone()); state.content_box = state.content_box.union( &fragment .base .rect() .translate(traversal_state.origin.to_vector()), ); } } impl Fragment { fn build_display_list_for_text_fragment( fragment: &TextFragment, builder: &mut DisplayListBuilder, state: &TraversalState, containing_block: &PhysicalRect<Au>, ) { // NB: The order of painting text components (CSS Text Decoration Module Level 3) is: // shadows, underline, overline, text, text-emphasis, and then line-through. let rect = fragment .base .rect() .translate(containing_block.origin.to_vector()); let mut baseline_origin = rect.origin; baseline_origin.y += fragment.font_metrics.ascent; let include_whitespace = fragment.run_data.selection.is_some() || state .text_decorations .iter() .any(|item| !item.line.is_empty()); let (glyphs, largest_advance) = glyphs( &fragment.glyphs, baseline_origin, fragment.justification_adjustment, include_whitespace, ); if glyphs.is_empty() && !fragment.is_empty_for_text_cursor { return; } let parent_style = fragment.style(); let color = parent_style.clone_color(); let font_size = parent_style.clone_font_size(); let font_metrics = &fragment.font_metrics; let dppx = builder.device_pixel_ratio.get(); let resolve_thickness = |thickness: &TextDecorationThickness| -> Au { let resolved = match thickness { TextDecorationThickness::LengthPercentage(length_percentage) => { length_percentage.resolve(font_size.computed_size.0).px() }, TextDecorationThickness::Auto | TextDecorationThickness::FromFont => { font_metrics.underline_size.to_f32_px() }, }; // If zero, return zero. // Else round down to the nearest physical pixel; floor at 1 physical pixel. // See: <https://drafts.csswg.org/css-values-4/#snap-as-a-line-width> if resolved == 0.0 { Au::zero() } else { Au::from_f32_px((resolved * dppx).floor().max(1.0) / dppx) } }; // Gecko gets the text bounding box based on the ink overflow bounds. Since // we don't need to calculate this yet (as we do not implement `contain: // paint`), we just need to make sure these boundaries are big enough to // contain the inked portion of the glyphs. We assume that the descent and // ascent are big enough and then just expand the advance-based boundaries by // twice the size of the biggest advance in the advance dimention. let glyph_bounds = rect .inflate(largest_advance.scale_by(2.0), Au::zero()) .to_webrender(); let common = builder.common_properties(state, glyph_bounds, &parent_style); // Shadows. According to CSS-BACKGROUNDS, text shadows render in *reverse* order (front to // back). let shadows = &parent_style.get_inherited_text().text_shadow; for shadow in shadows.0.iter().rev() { builder.wr().push_shadow( &wr::SpaceAndClipInfo { spatial_id: common.spatial_id, clip_chain_id: common.clip_chain_id, }, wr::Shadow { offset: LayoutVector2D::new(shadow.horizontal.px(), shadow.vertical.px()), color: rgba(shadow.color.resolve_to_absolute(&color)), blur_radius: shadow.blur.px(), }, true, /* should_inflate */ ); } Self::build_display_list_for_text_selection( fragment, builder, state, containing_block, fragment.base.rect().min_x(), fragment.justification_adjustment, ); for text_decoration in state.text_decorations.iter() { if text_decoration.line.contains(TextDecorationLine::UNDERLINE) { let mut rect = rect; rect.origin.y += font_metrics.ascent - font_metrics.underline_offset; rect.size.height = resolve_thickness(&text_decoration.thickness); Self::build_display_list_for_text_decoration( state, &parent_style, builder, &rect, text_decoration, TextDecorationLine::UNDERLINE, ); } } for text_decoration in state.text_decorations.iter() { if text_decoration.line.contains(TextDecorationLine::OVERLINE) { let mut rect = rect; rect.size.height = resolve_thickness(&text_decoration.thickness); Self::build_display_list_for_text_decoration( state, &parent_style, builder, &rect, text_decoration, TextDecorationLine::OVERLINE, ); } } builder.wr().push_text( &common, glyph_bounds, &glyphs, fragment.font_key, rgba(color), None, ); builder.check_if_paintable(glyph_bounds, common.clip_rect, parent_style.clone_opacity()); // From <https://www.w3.org/TR/paint-timing/#contentful>: // An element target is contentful when one or more of the following apply: // > target has a text node child, representing non-empty text, and the node’s used opacity is greater than zero. builder.mark_is_contentful(); for text_decoration in state.text_decorations.iter() { if text_decoration .line .contains(TextDecorationLine::LINE_THROUGH) { let mut rect = rect; rect.origin.y += font_metrics.ascent - font_metrics.strikeout_offset; rect.size.height = resolve_thickness(&text_decoration.thickness); Self::build_display_list_for_text_decoration( state, &parent_style, builder, &rect, text_decoration, TextDecorationLine::LINE_THROUGH, ); } } if !shadows.0.is_empty() { builder.wr().pop_all_shadows(); } } fn build_display_list_for_text_decoration( state: &TraversalState, parent_style: &ServoArc<ComputedValues>, builder: &mut DisplayListBuilder, rect: &PhysicalRect<Au>, text_decoration: &FragmentTextDecoration, line: TextDecorationLine, ) { if text_decoration.style == ComputedTextDecorationStyle::MozNone { return; } let mut rect = rect.to_webrender(); let wavy_line_thickness = rect.height().ceil(); if text_decoration.style == ComputedTextDecorationStyle::Wavy { rect = rect.inflate(0.0, wavy_line_thickness); } // In Servo, text decorations can span multiple text fragments. In order to have dots, // dashes, and wavy line segments match up between multiple fragments, this code extends // the painting rect for the decoration types for which this matters to the origin. As // the rectangle starts at the origin, all painted decorations will be in phase. As the // clipping rectangle is left unchanged, the actual painted region remains the size of // the original rectangle. let expand_rect_for_text_decoration = |mut rect: Box2D<f32, LayoutPixel>| { if matches!( text_decoration.style, ComputedTextDecorationStyle::Dotted | ComputedTextDecorationStyle::Dashed | ComputedTextDecorationStyle::Wavy, ) { rect.min.x = rect.min.x.min(0.0); } rect }; let common_properties = builder.common_properties(state, rect, parent_style); builder.wr().push_line( &common_properties, &expand_rect_for_text_decoration(rect), wavy_line_thickness, wr::LineOrientation::Horizontal, &rgba(text_decoration.color), text_decoration.style.to_webrender(), ); if text_decoration.style == TextDecorationStyle::Double { let half_height = (rect.height() / 2.0).floor().max(1.0); let y_offset = match line { TextDecorationLine::OVERLINE => -rect.height() - half_height, _ => rect.height() + half_height, }; let rect = rect.translate(Vector2D::new(0.0, y_offset)); let common_properties = builder.common_properties(state, rect, parent_style); builder.wr().push_line( &common_properties, &rect, wavy_line_thickness, wr::LineOrientation::Horizontal, &rgba(text_decoration.color), text_decoration.style.to_webrender(), ); } } fn build_display_list_for_broken_image_border( builder: &mut DisplayListBuilder, containing_block: &PhysicalRect<Au>, common: &CommonItemProperties, ) { let border_side = BorderSide { color: ColorF::BLACK, style: wr::BorderStyle::Inset, }; builder.wr().push_border( common, containing_block.to_webrender(), LayoutSideOffsets::new_all_same(1.0), BorderDetails::Normal(NormalBorder { left: border_side, right: border_side, top: border_side, bottom: border_side, radius: BorderRadius::zero(), do_aa: true, }), ); } // TODO: This caret/text selection implementation currently does not account for vertical text // and RTL text properly. fn build_display_list_for_text_selection( fragment: &TextFragment, builder: &mut DisplayListBuilder<'_>, state: &TraversalState, containing_block_rect: &PhysicalRect<Au>, fragment_x_offset: Au, justification_adjustment: Au, ) { let run_data = &fragment.run_data; let Some(shared_selection) = &run_data.selection else { return; }; let shared_selection = shared_selection.borrow(); if !shared_selection.enabled { return; } // The selection character range is in pre-transformed character offsets, so use the // OffsetMap contained within `run_data` to convert it to post-transformed character // offsets. This allows updating this selection directly from the DOM (skipping layout). let dom_selection_range = &shared_selection.character_range; let selection_character_range = run_data.map_dom_range_to_transformed_range( Utf32CodeUnits(dom_selection_range.start)..Utf32CodeUnits(dom_selection_range.end), ); if fragment.character_range_in_dom_node.start > selection_character_range.end || fragment.character_range_in_dom_node.end < selection_character_range.start { return; } // When there is an active selection, the line is empty, and there is a forced linebreak, // layout will push an empty fragment in order to trigger painting of the cursor on an empty line. // This code ensure that it is only painted if the cursor is on the starting index of the empty // fragment. if fragment.is_empty_for_text_cursor && !fragment .character_range_in_dom_node .contains(&selection_character_range.start) { return; } let mut current_character_index = fragment.character_range_in_dom_node.start; let mut current_advance = Au::zero(); let mut start_advance = None; let mut end_advance = None; for glyph_store in fragment.glyphs.iter() { let glyph_store_character_count = Utf32CodeUnits(glyph_store.character_count()); if current_character_index + glyph_store_character_count < selection_character_range.start { current_advance += glyph_store.total_advance() + (justification_adjustment * glyph_store.total_word_separators() as i32); current_character_index += glyph_store_character_count; continue; } if current_character_index >= selection_character_range.end { break; } for glyph in glyph_store.glyphs() { if current_character_index >= selection_character_range.start { start_advance = start_advance.or(Some(current_advance)); } current_character_index += Utf32CodeUnits(glyph.character_count()); current_advance += glyph.advance(); if glyph.char_is_word_separator() { current_advance += justification_adjustment; } if current_character_index <= selection_character_range.end { end_advance = Some(current_advance); } } } let start_x = start_advance.unwrap_or(current_advance); let end_x = end_advance.unwrap_or(current_advance); let parent_style = fragment.style(); if !selection_character_range.is_empty() { let selection_rect = Rect::new( containing_block_rect.origin + Vector2D::new(fragment_x_offset + start_x, Au::zero()), Size2D::new(end_x - start_x, containing_block_rect.height()), ) .to_webrender(); if let Some(selection_color) = fragment .selected_style() .clone_background_color() .as_absolute() { let selection_common = builder.common_properties(state, selection_rect, &parent_style); builder .wr() .push_rect(&selection_common, selection_rect, rgba(*selection_color)); } return; } let insertion_point_rect = Rect::new( containing_block_rect.origin + Vector2D::new(start_x + fragment_x_offset, Au::zero()), Size2D::new( INSERTION_POINT_LOGICAL_WIDTH, containing_block_rect.height(), ), ) .to_webrender(); let color = parent_style.clone_color(); let caret_color = match parent_style.clone_caret_color().0 { ColorOrAuto::Color(caret_color) => caret_color.resolve_to_absolute(&color), ColorOrAuto::Auto => color, }; let insertion_point_common = builder.common_properties(state, insertion_point_rect, &parent_style); let caret_color = rgba(caret_color); let property_binding = if prefs::get().editing_caret_blink_time().is_some() { // It's okay to always use the same property binding key for this pipeline, as // there is currently only a single thing that animates in this way (the caret). // This code should be updated if we ever add more paint-side animations. let pipeline_id: PipelineId = builder.paint_info.pipeline_id.into(); let property_binding_key = PropertyBindingKey::new(pipeline_id.into()); builder.paint_info.caret_property_binding = Some((property_binding_key, caret_color)); PropertyBinding::Binding(property_binding_key, caret_color) } else { PropertyBinding::Value(caret_color) }; builder.wr().push_rect_with_animation( &insertion_point_common, insertion_point_rect, property_binding, ); } } struct BuilderForBoxFragment<'a> { fragment: &'a BoxFragmentWithStyle<'a>, containing_block_origin: PhysicalPoint<Au>, border_rect: units::LayoutRect, margin_rect: OnceCell<units::LayoutRect>, padding_rect: OnceCell<units::LayoutRect>, content_rect: OnceCell<units::LayoutRect>, border_radius: OnceCell<wr::BorderRadius>, border_edge_clip_chain_id: RefCell<Option<ClipChainId>>, padding_edge_clip_chain_id: RefCell<Option<ClipChainId>>, content_edge_clip_chain_id: RefCell<Option<ClipChainId>>, } impl<'a> BuilderForBoxFragment<'a> { fn new( fragment: &'a BoxFragmentWithStyle<'a>, containing_block_origin: PhysicalPoint<Au>, ) -> Self { let border_rect = fragment .border_rect() .translate(containing_block_origin.to_vector()); Self { fragment, containing_block_origin, border_rect: border_rect.to_webrender(), border_radius: OnceCell::new(), margin_rect: OnceCell::new(), padding_rect: OnceCell::new(), content_rect: OnceCell::new(), border_edge_clip_chain_id: RefCell::new(None), padding_edge_clip_chain_id: RefCell::new(None), content_edge_clip_chain_id: RefCell::new(None), } } fn border_radius(&self) -> BorderRadius { *self .border_radius .get_or_init(|| self.fragment.border_radius()) } fn content_rect(&self) -> &units::LayoutRect { self.content_rect.get_or_init(|| { self.fragment .content_rect() .translate(self.containing_block_origin.to_vector()) .to_webrender() }) } fn padding_rect(&self) -> &units::LayoutRect { self.padding_rect.get_or_init(|| { self.fragment .padding_rect() .translate(self.containing_block_origin.to_vector()) .to_webrender() }) } fn margin_rect(&self) -> &units::LayoutRect { self.margin_rect.get_or_init(|| { self.fragment .margin_rect() .translate(self.containing_block_origin.to_vector()) .to_webrender() }) } fn border_edge_clip( &self, builder: &mut DisplayListBuilder, state: &TraversalState, force_clip_creation: bool, ) -> Option<ClipChainId> { if let Some(clip) = *self.border_edge_clip_chain_id.borrow() { return Some(clip); } let maybe_clip = builder.maybe_create_clip( state, self.border_radius(), self.border_rect, force_clip_creation, ); *self.border_edge_clip_chain_id.borrow_mut() = maybe_clip; maybe_clip } fn padding_edge_clip( &self, builder: &mut DisplayListBuilder, state: &TraversalState, force_clip_creation: bool, ) -> Option<ClipChainId> { if let Some(clip) = *self.padding_edge_clip_chain_id.borrow() { return Some(clip); } let radii = offset_radii(self.border_radius(), -self.fragment.border.to_webrender()); let maybe_clip = builder.maybe_create_clip(state, radii, *self.padding_rect(), force_clip_creation); *self.padding_edge_clip_chain_id.borrow_mut() = maybe_clip; maybe_clip } fn content_edge_clip( &self, builder: &mut DisplayListBuilder, state: &TraversalState, force_clip_creation: bool, ) -> Option<ClipChainId> { if let Some(clip) = *self.content_edge_clip_chain_id.borrow() { return Some(clip); } let radii = offset_radii( self.border_radius(), -(self.fragment.border + self.fragment.padding).to_webrender(), ); let maybe_clip = builder.maybe_create_clip(state, radii, *self.content_rect(), force_clip_creation); *self.content_edge_clip_chain_id.borrow_mut() = maybe_clip; maybe_clip } fn build(&mut self, builder: &mut DisplayListBuilder, state: &TraversalState) { if self .fragment .base .flags .contains(FragmentFlags::DO_NOT_PAINT) { return; } self.build_background(builder, state); self.build_box_shadow(builder, state); if !self.fragment.is_table_grid_with_collapsed_borders() { self.build_border(builder, state); } let overflow = self .fragment .style() .effective_overflow(self.fragment.base.flags); let scrolls_via_user_input = |overflow| matches!(overflow, ComputedOverflow::Scroll | ComputedOverflow::Auto); if (scrolls_via_user_input(overflow.x) || scrolls_via_user_input(overflow.y)) && self.fragment.style().get_inherited_ui().pointer_events != style::computed_values::pointer_events::T::None { let mut inner_state = state.clone(); inner_state.spatial_id = self .fragment .generated_scroll_tree_node_id() .unwrap_or(state.spatial_id); inner_state.clip_id = self.fragment.generated_clip_id().unwrap_or(state.clip_id); self.build_hit_test( builder, &inner_state, self.fragment .scrollable_overflow() .translate(self.containing_block_origin.to_vector()) .to_webrender(), ); } } fn build_hit_test( &self, builder: &mut DisplayListBuilder, state: &TraversalState, rect: LayoutRect, ) { let external_scroll_node_id = builder .paint_info .external_scroll_id_for_scroll_tree_node(state.spatial_id); let mut common = builder.common_properties(state, rect, self.fragment.style()); if let Some(clip_chain_id) = self.border_edge_clip(builder, state, false) { common.clip_chain_id = clip_chain_id; } builder.wr().push_hit_test( common.clip_rect, common.clip_chain_id, common.spatial_id, common.flags, (external_scroll_node_id.0, 0), /* tag */ ); } fn build_background_for_painter( &mut self, builder: &mut DisplayListBuilder, state: &TraversalState, painter: &BackgroundPainter, ) { let b = painter.style.get_background(); let background_color = painter.style.resolve_color(&b.background_color); if background_color.alpha > 0.0 { // https://drafts.csswg.org/css-backgrounds/#background-color // “The background color is clipped according to the background-clip // value associated with the bottom-most background image layer.” let layer_index = b.background_image.0.len() - 1; let bounds = painter.painting_area(self, builder, layer_index); let common = painter.common_properties(self, builder, state, layer_index, bounds); builder .wr() .push_rect(&common, bounds, rgba(background_color)); // From <https://www.w3.org/TR/paint-timing/#sec-terminology>: // First paint ... includes non-default background paint and the enclosing box of an iframe. // The spec is vague. See also: https://github.com/w3c/paint-timing/issues/122 let default_background_color = servo_config::pref!(shell_background_color_rgba); let default_background_color = AbsoluteColor::new( ColorSpace::Srgb, default_background_color[0] as f32, default_background_color[1] as f32, default_background_color[2] as f32, default_background_color[3] as f32, ) .into_srgb_legacy(); if background_color != default_background_color { builder.mark_is_paintable(); } } self.build_background_image(builder, state, painter); } fn build_background(&mut self, builder: &mut DisplayListBuilder, state: &TraversalState) { let flags = self.fragment.base.flags; // The root element's background is painted separately as it might inherit the `<body>`'s // background. if flags.intersects(FragmentFlags::IS_ROOT_ELEMENT) { return; } // If the `<body>` background was inherited by the root element, don't paint it again here. if !builder.paint_body_background && flags.intersects(FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT) { return; } // If this BoxFragment does not paint a background, do nothing. if let BackgroundMode::None = self.fragment.background_mode { return; } // Paint all extra backgrounds for this BoxFragment. These are painted first, as that's // the order that they are expected to be painted for table cells (where this feature // is used). if let BackgroundMode::Extra(ref extra_backgrounds) = self.fragment.background_mode { for extra_background in extra_backgrounds { let positioning_area = extra_background.rect; let painter = BackgroundPainter { style: &extra_background.style.borrow_mut(), painting_area_override: None, positioning_area_override: Some( positioning_area .translate(self.containing_block_origin.to_vector()) .to_webrender(), ), }; self.build_background_for_painter(builder, state, &painter); } } let painter = BackgroundPainter { style: self.fragment.style(), painting_area_override: None, positioning_area_override: None, }; self.build_background_for_painter(builder, state, &painter); } fn build_background_image( &self, builder: &mut DisplayListBuilder, state: &TraversalState, painter: &BackgroundPainter, ) { let style = painter.style; let b = style.get_background(); let need_blend_container = b .background_blend_mode .0 .iter() .take(b.background_image.0.len()) .any(|background_blend_mode| background_blend_mode != &BackgroundBlendMode::Normal); let push_stacking_context = |builder: &mut DisplayListBuilder, blend_mode: BackgroundBlendMode, flags: StackingContextFlags| -> bool { let spatial_id = builder.spatial_id(state.spatial_id); builder.wr().push_stacking_context( spatial_id, PrimitiveFlags::empty(), None, TransformStyle::Flat, blend_mode.to_webrender(), &[], &[], RasterSpace::Screen, flags, None, ); true }; if need_blend_container { push_stacking_context( builder, BackgroundBlendMode::Normal, StackingContextFlags::IS_BLEND_CONTAINER, ); } let node = self.fragment.base.tag.map(|tag| tag.node); // Reverse because the property is top layer first, we want to paint bottom layer first. for (index, image) in b.background_image.0.iter().enumerate().rev() { let Ok(resolved_image) = builder.image_resolver.resolve_image(node, image) else { continue; }; match resolved_image { ResolvedImage::Gradient(_) | ResolvedImage::Color(_) => { let intrinsic = NaturalSizes::empty(); let Some(layer) = &background::layout_layer(self, painter, builder, state, index, intrinsic) else { continue; }; let needs_blending = layer.blend_mode != BackgroundBlendMode::Normal; if needs_blending { push_stacking_context(builder, layer.blend_mode, Default::default()); } match resolved_image { ResolvedImage::Gradient(gradient) => { match gradient::build(style, gradient, layer.tile_size, builder) { WebRenderGradient::Linear(linear_gradient) => { builder.wr().push_gradient( &layer.common, layer.bounds, linear_gradient, layer.tile_size, layer.tile_spacing, ) }, WebRenderGradient::Radial(radial_gradient) => { builder.wr().push_radial_gradient( &layer.common, layer.bounds, radial_gradient, layer.tile_size, layer.tile_spacing, ) }, WebRenderGradient::Conic(conic_gradient) => { builder.wr().push_conic_gradient( &layer.common, layer.bounds, conic_gradient, layer.tile_size, layer.tile_spacing, ) }, } }, ResolvedImage::Color(color) => { let color = rgba(style.resolve_color(color)); builder.wr().push_rect(&layer.common, layer.bounds, color); }, _ => {}, } if needs_blending { builder.wr().pop_stacking_context(); } builder.check_if_paintable( layer.bounds, layer.common.clip_rect, style.clone_opacity(), ); }, ResolvedImage::Image { image, size } => { // FIXME: https://drafts.csswg.org/css-images-4/#the-image-resolution let dppx = 1.0; let intrinsic = NaturalSizes::from_width_and_height(size.width / dppx, size.height / dppx); let layer = background::layout_layer(self, painter, builder, state, index, intrinsic); let image_wr_key = match image { CachedImage::Raster(raster_image) => raster_image.id, CachedImage::Vector(vector_image) => { let scale = builder.device_pixel_ratio.get(); let default_size: DeviceIntSize = Size2D::new(size.width * scale, size.height * scale).to_i32(); let layer_size = layer.as_ref().map(|layer| { Size2D::new( layer.tile_size.width * scale, layer.tile_size.height * scale, ) .to_i32() }); node.and_then(|node| { let size = layer_size.unwrap_or(default_size); builder.image_resolver.rasterize_vector_image( vector_image.id, size, node, vector_image.svg_id, ) }) .and_then(|rasterized_image| rasterized_image.id) }, }; let Some(image_key) = image_wr_key else { continue; }; if let Some(layer) = layer { let needs_blending = layer.blend_mode != BackgroundBlendMode::Normal; if needs_blending { push_stacking_context(builder, layer.blend_mode, Default::default()); } if layer.repeat { builder.wr().push_repeating_image( &layer.common, layer.bounds, layer.tile_size, layer.tile_spacing, style.clone_image_rendering().to_webrender(), wr::AlphaType::PremultipliedAlpha, image_key, wr::ColorF::WHITE, ) } else { builder.wr().push_image( &layer.common, layer.bounds, style.clone_image_rendering().to_webrender(), wr::AlphaType::PremultipliedAlpha, image_key, wr::ColorF::WHITE, ) } if needs_blending { builder.wr().pop_stacking_context(); } builder.check_if_paintable( layer.bounds, layer.common.clip_rect, style.clone_opacity(), ); // From <https://www.w3.org/TR/paint-timing/#sec-terminology>: // An element target is contentful when one or more of the following apply: // > target has a background-image which is a contentful image, and its used // > background-size has non-zero width and height values. builder.mark_is_contentful(); let natural_width = Some(Au::from_f32_px(size.width / dppx)); let natural_height = Some(Au::from_f32_px(size.height / dppx)); builder.check_for_lcp_candidate( state, layer.bounds, layer.common.clip_rect, self.fragment.base.tag, None, natural_width, natural_height, ); } }, } } if need_blend_container { builder.wr().pop_stacking_context(); } } fn build_border_side(&self, style_color: BorderStyleColor) -> wr::BorderSide { wr::BorderSide { color: rgba(style_color.color), style: match style_color.style { BorderStyle::None => wr::BorderStyle::None, BorderStyle::Solid => wr::BorderStyle::Solid, BorderStyle::Double => wr::BorderStyle::Double, BorderStyle::Dotted => wr::BorderStyle::Dotted, BorderStyle::Dashed => wr::BorderStyle::Dashed, BorderStyle::Hidden => wr::BorderStyle::Hidden, BorderStyle::Groove => wr::BorderStyle::Groove, BorderStyle::Ridge => wr::BorderStyle::Ridge, BorderStyle::Inset => wr::BorderStyle::Inset, BorderStyle::Outset => wr::BorderStyle::Outset, }, } } fn build_collapsed_table_borders( &self, builder: &mut DisplayListBuilder, state: &TraversalState, ) { if self .fragment .base .flags .contains(FragmentFlags::DO_NOT_PAINT) { return; } let layout_info = self.fragment.specific_layout_info(); let Some(SpecificLayoutInfo::TableGridWithCollapsedBorders(table_info)) = layout_info.as_deref() else { return; }; let mut common = builder.common_properties(state, units::LayoutRect::default(), self.fragment.style()); let radius = wr::BorderRadius::default(); let mut column_sum = Au::zero(); for (x, column_size) in table_info.track_sizes.x.iter().enumerate() { let mut row_sum = Au::zero(); for (y, row_size) in table_info.track_sizes.y.iter().enumerate() { let left_border = &table_info.collapsed_borders.x[x][y]; let right_border = &table_info.collapsed_borders.x[x + 1][y]; let top_border = &table_info.collapsed_borders.y[y][x]; let bottom_border = &table_info.collapsed_borders.y[y + 1][x]; let details = wr::BorderDetails::Normal(wr::NormalBorder { left: self.build_border_side(left_border.style_color.clone()), right: self.build_border_side(right_border.style_color.clone()), top: self.build_border_side(top_border.style_color.clone()), bottom: self.build_border_side(bottom_border.style_color.clone()), radius, do_aa: true, }); let mut border_widths = PhysicalSides::new( top_border.width, right_border.width, bottom_border.width, left_border.width, ); let left_adjustment = if x == 0 { -border_widths.left / 2 } else { std::mem::take(&mut border_widths.left) / 2 }; let top_adjustment = if y == 0 { -border_widths.top / 2 } else { std::mem::take(&mut border_widths.top) / 2 }; let origin = PhysicalPoint::new(column_sum + left_adjustment, row_sum + top_adjustment); let size = PhysicalSize::new( *column_size - left_adjustment + border_widths.right / 2, *row_size - top_adjustment + border_widths.bottom / 2, ); let border_rect = PhysicalRect::new(origin, size) .translate(self.fragment.content_rect().origin.to_vector()) .translate(self.containing_block_origin.to_vector()) .to_webrender(); common.clip_rect = border_rect; builder.wr().push_border( &common, border_rect, border_widths.to_webrender(), details, ); row_sum += *row_size; } column_sum += *column_size; } } fn build_border(&mut self, builder: &mut DisplayListBuilder, state: &TraversalState) { if self.fragment.has_collapsed_borders() { // Avoid painting borders for tables and table parts in collapsed-borders mode, // since the resulting collapsed borders are painted on their own in a special way. return; } let style = self.fragment.style(); let border = style.get_border(); let border_widths = self.fragment.border.to_webrender(); if border_widths == SideOffsets2D::zero() { return; } // `border-image` replaces an element's border entirely. if self.build_border_image(builder, state, border, border_widths) { return; } let current_color = style.get_inherited_text().clone_color(); let style_color = BorderStyleColor::from_border(border, ¤t_color); let details = wr::BorderDetails::Normal(wr::NormalBorder { top: self.build_border_side(style_color.top), right: self.build_border_side(style_color.right), bottom: self.build_border_side(style_color.bottom), left: self.build_border_side(style_color.left), radius: self.border_radius(), do_aa: true, }); let common = builder.common_properties(state, self.border_rect, style); builder .wr() .push_border(&common, self.border_rect, border_widths, details) } /// Add a display item for image borders if necessary. fn build_border_image( &self, builder: &mut DisplayListBuilder, state: &TraversalState, border: &Border, border_widths: SideOffsets2D<f32, LayoutPixel>, ) -> bool { let style = self.fragment.style(); let border_style_struct = style.get_border(); let border_image_outset = resolve_border_image_outset(border_style_struct.border_image_outset, border_widths); let border_image_area = self.border_rect.to_rect().outer_rect(border_image_outset); let border_image_size = border_image_area.size; let border_image_widths = resolve_border_image_width( &border_style_struct.border_image_width, border_widths, border_image_size, ); let border_image_repeat = &border_style_struct.border_image_repeat; let border_image_fill = border_style_struct.border_image_slice.fill; let border_image_slice = &border_style_struct.border_image_slice.offsets; let common = builder.common_properties(state, border_image_area.to_box2d(), style); let stops = Vec::new(); let mut width = border_image_size.width; let mut height = border_image_size.height; let node = self.fragment.base.tag.map(|tag| tag.node); let source = match builder .image_resolver .resolve_image(node, &border.border_image_source) { Err(_) => return false, Ok(ResolvedImage::Image { image, size }) => { let image_key = match image { CachedImage::Raster(raster_image) => raster_image.id, CachedImage::Vector(vector_image) => { let scale = builder.device_pixel_ratio.get(); let size = Size2D::new(size.width * scale, size.height * scale).to_i32(); node.and_then(|node| { builder.image_resolver.rasterize_vector_image( vector_image.id, size, node, vector_image.svg_id, ) }) .and_then(|rasterized_image| rasterized_image.id) }, }; let Some(key) = image_key else { return false; }; builder.check_if_paintable( Box2D::from_size(size.cast_unit()), common.clip_rect, style.clone_opacity(), ); // From <https://www.w3.org/TR/paint-timing/#contentful>: // An element target is contentful when one or more of the following apply: // > target has a background-image which is a contentful image, // > and its used background-size has non-zero width and height values. builder.mark_is_contentful(); width = size.width; height = size.height; let image_rendering = style.clone_image_rendering().to_webrender(); NinePatchBorderSource::Image(key, image_rendering) }, Ok(ResolvedImage::Gradient(gradient)) => { match gradient::build(style, gradient, border_image_size, builder) { WebRenderGradient::Linear(gradient) => { NinePatchBorderSource::Gradient(gradient) }, WebRenderGradient::Radial(gradient) => { NinePatchBorderSource::RadialGradient(gradient) }, WebRenderGradient::Conic(gradient) => { NinePatchBorderSource::ConicGradient(gradient) }, } }, Ok(ResolvedImage::Color(color)) => { // NinePatchBorderSource doesn't support a lone color, so pretend that // its a linear gradient. let color = rgba(style.resolve_color(color)); let gradient = builder.wr().create_gradient( Point2D::zero(), Point2D::zero(), vec![ wr::GradientStop { offset: 0.0, color }, wr::GradientStop { offset: 1.0, color }, ], wr::ExtendMode::Clamp, ); NinePatchBorderSource::Gradient(gradient) }, }; let size = Size2D::new(width as i32, height as i32); // If the size of the border is zero or the size of the border image is zero, just // don't render anything. Zero-sized gradients cause problems in WebRender. if size.is_empty() || border_image_size.is_empty() { return true; } let details = BorderDetails::NinePatch(NinePatchBorder { source, width: size.width, height: size.height, slice: resolve_border_image_slice(border_image_slice, size), fill: border_image_fill, repeat_horizontal: border_image_repeat.0.to_webrender(), repeat_vertical: border_image_repeat.1.to_webrender(), }); builder.wr().push_border( &common, border_image_area.to_box2d(), border_image_widths, details, ); builder.wr().push_stops(&stops); true } fn build_outline(&self, builder: &mut DisplayListBuilder, state: &TraversalState) { let style = self.fragment.style(); let outline = style.get_outline(); if outline.outline_style.none_or_hidden() { return; } let width = outline.outline_width.0.to_f32_px(); if width == 0.0 { return; } // <https://drafts.csswg.org/css-ui-3/#outline-offset> // > Negative values must cause the outline to shrink into the border box. Both // > the height and the width of outside of the shape drawn by the outline should // > not become smaller than twice the computed value of the outline-width // > property, to make sure that an outline can be rendered even with large // > negative values. User agents should apply this constraint independently in // > each dimension. If the outline is drawn as multiple disconnected shapes, this // > constraint applies to each shape separately. let offset = outline.outline_offset.to_f32_px() + width; let outline_rect = self.border_rect.inflate( offset.max(-self.border_rect.width() / 2.0 + width), offset.max(-self.border_rect.height() / 2.0 + width), ); let common = builder.common_properties(state, outline_rect, style); let widths = SideOffsets2D::new_all_same(width); let border_style = match outline.outline_style { // TODO: treating 'auto' as 'solid' is allowed by the spec, // but we should do something better. OutlineStyle::Auto => BorderStyle::Solid, OutlineStyle::BorderStyle(s) => s, }; let side = self.build_border_side(BorderStyleColor { style: border_style, color: style.resolve_color(&outline.outline_color), }); let details = wr::BorderDetails::Normal(wr::NormalBorder { top: side, right: side, bottom: side, left: side, radius: offset_radii(self.border_radius(), SideOffsets2D::new_all_same(offset)), do_aa: true, }); builder .wr() .push_border(&common, outline_rect, widths, details) } fn build_box_shadow(&self, builder: &mut DisplayListBuilder, state: &TraversalState) { let style = self.fragment.style(); let box_shadows = &style.get_effects().box_shadow.0; if box_shadows.is_empty() { return; } // Note: According to CSS-BACKGROUNDS, box shadows render in *reverse* order (front to back). for box_shadow in box_shadows.iter().rev() { let (rect, clip_mode) = if box_shadow.inset { (*self.padding_rect(), BoxShadowClipMode::Inset) } else { (self.border_rect, BoxShadowClipMode::Outset) }; let offset = LayoutVector2D::new( box_shadow.base.horizontal.px(), box_shadow.base.vertical.px(), ); let spread = box_shadow.spread.px(); let blur = box_shadow.base.blur.px(); let clip_rect = match clip_mode { // Inset shadows are always inside the rect. BoxShadowClipMode::Inset => rect, // Match webrender's box_shadow.rs Gaussian blur inflation. // (BLUR_SAMPLE_SCALE * blur).ceil(). BLUR_SAMPLE_SCALE is 3.0. BoxShadowClipMode::Outset => { let extra_size_from_blur = (blur * 3.0).ceil(); rect.translate(offset) .inflate(spread, spread) .inflate(extra_size_from_blur, extra_size_from_blur) }, }; let border_radius = match clip_mode { BoxShadowClipMode::Inset => { // The `border-radius` value applies to the border box, but inset shadows // use the padding box instead. So we need to shrink the `border-radius` // by the border widths. offset_radii(self.border_radius(), -self.fragment.border.to_webrender()) }, BoxShadowClipMode::Outset => self.border_radius(), }; let shadow_radius = offset_radii( border_radius, SideOffsets2D::new_all_same(match clip_mode { BoxShadowClipMode::Inset => -spread, BoxShadowClipMode::Outset => spread, }), ); let common = builder.common_properties(state, clip_rect, style); builder.wr().push_box_shadow( &common, rect, offset, rgba(style.resolve_color(&box_shadow.base.color)), blur, spread, border_radius, shadow_radius, clip_mode, ); } } } fn rgba(color: AbsoluteColor) -> wr::ColorF { let rgba = color.to_color_space(ColorSpace::Srgb); wr::ColorF::new( rgba.components.0.clamp(0.0, 1.0), rgba.components.1.clamp(0.0, 1.0), rgba.components.2.clamp(0.0, 1.0), rgba.alpha.clamp(0.0, 1.0), ) } fn glyphs( shaped_text_slices: &[Arc<ShapedTextSlice>], mut baseline_origin: PhysicalPoint<Au>, justification_adjustment: Au, include_whitespace: bool, ) -> (Vec<GlyphInstance>, Au) { let mut glyphs = vec![]; let mut largest_advance = Au::zero(); for shaped_text_slice in shaped_text_slices { for glyph in shaped_text_slice.glyphs() { if !shaped_text_slice.is_whitespace() || include_whitespace { let glyph_offset = glyph.offset().unwrap_or(Point2D::zero()); let point = LayoutPoint::new( baseline_origin.x.to_f32_px() + glyph_offset.x.to_f32_px(), baseline_origin.y.to_f32_px() + glyph_offset.y.to_f32_px(), ); let glyph_instance = GlyphInstance { index: glyph.id(), point, }; glyphs.push(glyph_instance); } if glyph.char_is_word_separator() { baseline_origin.x += justification_adjustment; } let advance = glyph.advance(); baseline_origin.x += advance; largest_advance.max_assign(advance); } } (glyphs, largest_advance) } /// Given a set of corner radii for a rectangle, this function returns the corresponding radii /// for the [outer rectangle][`Rect::outer_rect`] resulting from expanding the original /// rectangle by the given offsets. fn offset_radii(mut radii: BorderRadius, offsets: LayoutSideOffsets) -> BorderRadius { let expand = |radius: &mut f32, offset: f32| { // For negative offsets, just shrink the radius by that amount. if offset < 0.0 { *radius = (*radius + offset).max(0.0); return; } // For positive offsets, expand the radius by that amount. But only if the // radius is positive, in order to preserve sharp corners. // TODO: this behavior is not continuous, we should use this algorithm instead: // https://github.com/w3c/csswg-drafts/issues/7103#issuecomment-3357331922 if *radius > 0.0 { *radius += offset; } }; if offsets.left != 0.0 { expand(&mut radii.top_left.width, offsets.left); expand(&mut radii.bottom_left.width, offsets.left); } if offsets.right != 0.0 { expand(&mut radii.top_right.width, offsets.right); expand(&mut radii.bottom_right.width, offsets.right); } if offsets.top != 0.0 { expand(&mut radii.top_left.height, offsets.top); expand(&mut radii.top_right.height, offsets.top); } if offsets.bottom != 0.0 { expand(&mut radii.bottom_right.height, offsets.bottom); expand(&mut radii.bottom_left.height, offsets.bottom); } radii } /// Resolve the WebRender border-image outset area from the style values. fn resolve_border_image_outset( outset: BorderImageOutset, border: SideOffsets2D<f32, LayoutPixel>, ) -> SideOffsets2D<f32, LayoutPixel> { fn image_outset_for_side(outset: NonNegativeLengthOrNumber, border_width: f32) -> f32 { match outset { NonNegativeLengthOrNumber::Length(length) => length.px(), NonNegativeLengthOrNumber::Number(factor) => border_width * factor.0, } } SideOffsets2D::new( image_outset_for_side(outset.0, border.top), image_outset_for_side(outset.1, border.right), image_outset_for_side(outset.2, border.bottom), image_outset_for_side(outset.3, border.left), ) } /// Resolve the WebRender border-image width from the style values. fn resolve_border_image_width( width: &BorderImageWidth, border: SideOffsets2D<f32, LayoutPixel>, border_area: Size2D<f32, LayoutPixel>, ) -> SideOffsets2D<f32, LayoutPixel> { fn image_width_for_side( border_image_width: &BorderImageSideWidth, border_width: f32, total_length: f32, ) -> f32 { match border_image_width { BorderImageSideWidth::LengthPercentage(v) => { v.to_used_value(Au::from_f32_px(total_length)).to_f32_px() }, BorderImageSideWidth::Number(x) => border_width * x.0, BorderImageSideWidth::Auto => border_width, } } SideOffsets2D::new( image_width_for_side(&width.0, border.top, border_area.height), image_width_for_side(&width.1, border.right, border_area.width), image_width_for_side(&width.2, border.bottom, border_area.height), image_width_for_side(&width.3, border.left, border_area.width), ) } /// Resolve the WebRender border-image slice from the style values. fn resolve_border_image_slice( border_image_slice: &StyleRect<NonNegative<NumberOrPercentage>>, size: Size2D<i32, UnknownUnit>, ) -> SideOffsets2D<i32, DevicePixel> { fn resolve_percentage(value: NonNegative<NumberOrPercentage>, length: i32) -> i32 { match value.0 { NumberOrPercentage::Percentage(p) => (p.0 * length as f32).round() as i32, NumberOrPercentage::Number(n) => n.round() as i32, } } SideOffsets2D::new( resolve_percentage(border_image_slice.0, size.height), resolve_percentage(border_image_slice.1, size.width), resolve_percentage(border_image_slice.2, size.height), resolve_percentage(border_image_slice.3, size.width), ) } pub(super) fn normalize_radii(rect: &units::LayoutRect, radius: &mut wr::BorderRadius) { // Normalize radii that add up to > 100%. // https://www.w3.org/TR/css-backgrounds-3/#corner-overlap // > Let f = min(L_i/S_i), where i ∈ {top, right, bottom, left}, // > S_i is the sum of the two corresponding radii of the corners on side i, // > and L_top = L_bottom = the width of the box, // > and L_left = L_right = the height of the box. // > If f < 1, then all corner radii are reduced by multiplying them by f. let f = (rect.width() / (radius.top_left.width + radius.top_right.width)) .min(rect.width() / (radius.bottom_left.width + radius.bottom_right.width)) .min(rect.height() / (radius.top_left.height + radius.bottom_left.height)) .min(rect.height() / (radius.top_right.height + radius.bottom_right.height)); if f < 1.0 { radius.top_left *= f; radius.top_right *= f; radius.bottom_right *= f; radius.bottom_left *= f; } } /// <https://drafts.csswg.org/css-shapes-1/#valdef-shape-box-margin-box> /// > The corner radii of this shape are determined by the corresponding /// > border-radius and margin values. If the ratio of border-radius/margin is 1 or more, /// > or margin is negative or zero, then the margin box corner radius is /// > max(border-radius + margin, 0). If the ratio of border-radius/margin is less than 1, /// > and margin is positive, then the margin box corner radius is /// > border-radius + margin * (1 + (ratio-1)^3). pub(super) fn compute_margin_box_radius( radius: wr::BorderRadius, layout_rect: LayoutSize, fragment: &BoxFragment, ) -> wr::BorderRadius { let style = fragment.style(); let margin = style.physical_margin(); let adjust_radius = |radius: f32, margin: f32| -> f32 { if margin <= 0. || (radius / margin) >= 1. { (radius + margin).max(0.) } else { radius + (margin * (1. + (radius / margin - 1.).powf(3.))) } }; let compute_margin_radius = |radius: LayoutSize, layout_rect: LayoutSize, margin: Size2D<LengthPercentageOrAuto, UnknownUnit>| -> LayoutSize { let zero = LengthPercentage::zero(); let width = margin .width .auto_is(|| &zero) .to_used_value(Au::from_f32_px(layout_rect.width)); let height = margin .height .auto_is(|| &zero) .to_used_value(Au::from_f32_px(layout_rect.height)); LayoutSize::new( adjust_radius(radius.width, width.to_f32_px()), adjust_radius(radius.height, height.to_f32_px()), ) }; wr::BorderRadius { top_left: compute_margin_radius( radius.top_left, layout_rect, Size2D::new(margin.left, margin.top), ), top_right: compute_margin_radius( radius.top_right, layout_rect, Size2D::new(margin.right, margin.top), ), bottom_left: compute_margin_radius( radius.bottom_left, layout_rect, Size2D::new(margin.left, margin.bottom), ), bottom_right: compute_margin_radius( radius.bottom_right, layout_rect, Size2D::new(margin.right, margin.bottom), ), } } impl BoxFragment { fn border_radius(&self) -> BorderRadius { let style = self.style(); let border = style.get_border(); if border.border_top_left_radius.0.is_zero() && border.border_top_right_radius.0.is_zero() && border.border_bottom_right_radius.0.is_zero() && border.border_bottom_left_radius.0.is_zero() { return BorderRadius::zero(); } let border_rect = self.border_rect(); let resolve = |radius: &LengthPercentage, box_size: Au| radius.to_used_value(box_size).to_f32_px(); let corner = |corner: &style::values::computed::BorderCornerRadius| { Size2D::new( resolve(&corner.0.width.0, border_rect.size.width), resolve(&corner.0.height.0, border_rect.size.height), ) }; let mut radius = wr::BorderRadius { top_left: corner(&border.border_top_left_radius), top_right: corner(&border.border_top_right_radius), bottom_right: corner(&border.border_bottom_right_radius), bottom_left: corner(&border.border_bottom_left_radius), }; normalize_radii(&border_rect.to_webrender(), &mut radius); radius } } impl BaseFragment { fn visit_fragment(&self, builder: &mut DisplayListBuilder) { match self.status() { FragmentStatus::New => { builder.reflow_statistics.rebuilt_fragment_count += 1; self.set_status(FragmentStatus::Clean) }, FragmentStatus::StyleChanged => { builder.reflow_statistics.restyle_fragment_count += 1; self.set_status(FragmentStatus::Clean) }, FragmentStatus::OnlyDescendantsChanged => { builder.reflow_statistics.only_descendants_changed_count += 1; self.set_status(FragmentStatus::Clean) }, FragmentStatus::Clean => {}, } } }