mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
layout: Resolve canvas background properties during painting (#36917)
Instead of resolving the canvas background properties (essentially keeping a possible reference to the `<body>`'s style) during fragment tree construction, wait until painting to possibly find the style on an appropriate `<body>` fragment. This is possible now because `Fragment` keeps a list of flags with relevant information about the root and `<body>` elements. A benefit of this approach is that styles aren't cached in the fragment tree, which would be problematic for incremental layout. In addition, the old code was making an effort to transform the `<body>`'s background by the root element's transform. Only Safari does this and there was a resolution the WG that this should not happen in https://github.com/w3c/csswg-drafts/issues/6683. Testing: - `/css/css-transforms/transform-translate-background-001.html` - `/css/css-transforms/transform-translate-background-002.html` - `/css/CSS2/floats/float-root.html` Fixes: #30475. Closes: #30569. Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
parent
c6f61e6b6e
commit
53be79a5b5
9 changed files with 116 additions and 195 deletions
|
@ -153,10 +153,6 @@ pub(crate) struct DisplayListBuilder<'a> {
|
|||
/// list building functions.
|
||||
current_clip_chain_id: ClipChainId,
|
||||
|
||||
/// The [OpaqueNode] handle to the node used to paint the page background
|
||||
/// if the background was a canvas.
|
||||
element_for_canvas_background: OpaqueNode,
|
||||
|
||||
/// A [LayoutContext] used to get information about the device pixel ratio
|
||||
/// and get handles to WebRender images.
|
||||
pub context: &'a LayoutContext<'a>,
|
||||
|
@ -169,6 +165,11 @@ pub(crate) struct DisplayListBuilder<'a> {
|
|||
/// 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,
|
||||
}
|
||||
|
||||
struct InspectorHighlight {
|
||||
|
@ -218,12 +219,12 @@ impl DisplayList {
|
|||
current_scroll_node_id: self.compositor_info.root_reference_frame_id,
|
||||
current_reference_frame_scroll_node_id: self.compositor_info.root_reference_frame_id,
|
||||
current_clip_chain_id: ClipChainId::INVALID,
|
||||
element_for_canvas_background: fragment_tree.canvas_background.from_element,
|
||||
context,
|
||||
display_list: self,
|
||||
inspector_highlight: context
|
||||
.highlighted_dom_node
|
||||
.map(InspectorHighlight::for_node),
|
||||
paint_body_background: true,
|
||||
};
|
||||
fragment_tree.build_display_list(&mut builder, root_stacking_context);
|
||||
|
||||
|
@ -999,12 +1000,17 @@ impl<'a> BuilderForBoxFragment<'a> {
|
|||
}
|
||||
|
||||
fn build_background(&mut self, builder: &mut DisplayListBuilder) {
|
||||
if self
|
||||
.fragment
|
||||
.base
|
||||
.is_for_node(builder.element_for_canvas_background)
|
||||
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)
|
||||
{
|
||||
// This background is already painted for the canvas, don’t paint it again here.
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -582,15 +582,42 @@ impl StackingContext {
|
|||
&self,
|
||||
builder: &mut DisplayListBuilder,
|
||||
fragment_tree: &crate::FragmentTree,
|
||||
containing_block_rect: &PhysicalRect<Au>,
|
||||
) {
|
||||
let style = if let Some(style) = &fragment_tree.canvas_background.style {
|
||||
style
|
||||
} else {
|
||||
// The root element has `display: none`,
|
||||
// or the canvas background is taken from `<body>` which has `display: none`
|
||||
let Some(root_fragment) = fragment_tree.root_fragments.iter().find(|fragment| {
|
||||
fragment
|
||||
.base()
|
||||
.is_some_and(|base| base.flags.intersects(FragmentFlags::IS_ROOT_ELEMENT))
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let root_fragment = match root_fragment {
|
||||
Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => box_fragment,
|
||||
_ => return,
|
||||
}
|
||||
.borrow();
|
||||
|
||||
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.
|
||||
if root_fragment.style.background_is_transparent() {
|
||||
let body_fragment = fragment_tree.body_fragment();
|
||||
builder.paint_body_background = body_fragment.is_none();
|
||||
body_fragment
|
||||
.map(|body_fragment| body_fragment.borrow().style.clone())
|
||||
.unwrap_or(root_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.
|
||||
|
@ -598,14 +625,15 @@ impl StackingContext {
|
|||
// 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 mut painting_area = fragment_tree
|
||||
let painting_area = fragment_tree
|
||||
.initial_containing_block
|
||||
.union(&fragment_tree.scrollable_overflow)
|
||||
.to_webrender();
|
||||
|
||||
let background_color = style.resolve_color(&style.get_background().background_color);
|
||||
let background_color =
|
||||
source_style.resolve_color(&source_style.get_background().background_color);
|
||||
if background_color.alpha > 0.0 {
|
||||
let common = builder.common_properties(painting_area, style);
|
||||
let common = builder.common_properties(painting_area, &source_style);
|
||||
let color = super::rgba(background_color);
|
||||
builder
|
||||
.display_list
|
||||
|
@ -613,97 +641,14 @@ impl StackingContext {
|
|||
.push_rect(&common, painting_area, color)
|
||||
}
|
||||
|
||||
// `background-color` was comparatively easy,
|
||||
// but `background-image` needs a positioning area based on the root element.
|
||||
// Let’s find the corresponding fragment.
|
||||
|
||||
// The fragment generated by the root element is the first one here, unless…
|
||||
let first_if_any = self.contents.first().or_else(|| {
|
||||
// There wasn’t any `StackingContextFragment` in the root `StackingContext`,
|
||||
// because the root element generates a stacking context. Let’s find that one.
|
||||
self.real_stacking_contexts_and_positioned_stacking_containers
|
||||
.first()
|
||||
.and_then(|first_child_stacking_context| {
|
||||
first_child_stacking_context.contents.first()
|
||||
})
|
||||
});
|
||||
|
||||
macro_rules! debug_panic {
|
||||
($msg: expr) => {
|
||||
if cfg!(debug_assertions) {
|
||||
panic!($msg);
|
||||
} else {
|
||||
warn!($msg);
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let first_stacking_context_fragment = if let Some(first) = first_if_any {
|
||||
first
|
||||
} else {
|
||||
// This should only happen if the root element has `display: none`
|
||||
// TODO(servo#30569) revert to debug_panic!() once underlying bug is fixed
|
||||
log::warn!(
|
||||
"debug assertion failed! `CanvasBackground::for_root_element` should have returned `style: None`",
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let StackingContextContent::Fragment {
|
||||
fragment,
|
||||
scroll_node_id,
|
||||
containing_block,
|
||||
..
|
||||
} = first_stacking_context_fragment
|
||||
else {
|
||||
debug_panic!("Expected a fragment, not a stacking container");
|
||||
};
|
||||
let box_fragment = match fragment {
|
||||
Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => box_fragment,
|
||||
_ => debug_panic!("Expected a box-generated fragment"),
|
||||
};
|
||||
let box_fragment = &*box_fragment.borrow();
|
||||
|
||||
// The `StackingContextFragment` we found is for the root DOM element:
|
||||
debug_assert_eq!(
|
||||
fragment.tag().map(|tag| tag.node),
|
||||
Some(fragment_tree.canvas_background.root_element),
|
||||
);
|
||||
|
||||
// The root element may have a CSS transform, and we want the canvas’
|
||||
// background image to be transformed. To do so, take its `SpatialId`
|
||||
// (but not its `ClipId`)
|
||||
builder.current_scroll_node_id = *scroll_node_id;
|
||||
|
||||
// Now we need express the painting area rectangle in the local coordinate system,
|
||||
// which differs from the top-level coordinate system based on…
|
||||
|
||||
// Convert the painting area rectangle to the local coordinate system of this `SpatialId`
|
||||
if let Some(reference_frame_data) =
|
||||
box_fragment.reference_frame_data_if_necessary(containing_block_rect)
|
||||
{
|
||||
painting_area.min -= reference_frame_data.origin.to_webrender().to_vector();
|
||||
if let Some(transformed) = reference_frame_data
|
||||
.transform
|
||||
.inverse()
|
||||
.and_then(|inversed| inversed.outer_transformed_rect(&painting_area.to_rect()))
|
||||
{
|
||||
painting_area = transformed.to_box2d();
|
||||
} else {
|
||||
// The desired rect cannot be represented, so skip painting this background-image
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut fragment_builder = BuilderForBoxFragment::new(
|
||||
box_fragment,
|
||||
containing_block,
|
||||
&root_fragment,
|
||||
&fragment_tree.initial_containing_block,
|
||||
false, /* is_hit_test_for_scrollable_overflow */
|
||||
false, /* is_collapsed_table_borders */
|
||||
);
|
||||
let painter = super::background::BackgroundPainter {
|
||||
style,
|
||||
style: &source_style,
|
||||
painting_area_override: Some(painting_area),
|
||||
positioning_area_override: None,
|
||||
};
|
||||
|
|
|
@ -52,7 +52,7 @@ pub mod inline;
|
|||
mod root;
|
||||
|
||||
pub(crate) use construct::BlockContainerBuilder;
|
||||
pub use root::{BoxTree, CanvasBackground};
|
||||
pub use root::BoxTree;
|
||||
|
||||
#[derive(Debug, MallocSizeOf)]
|
||||
pub(crate) struct BlockFormattingContext {
|
||||
|
|
|
@ -12,7 +12,7 @@ use script_layout_interface::wrapper_traits::{
|
|||
};
|
||||
use script_layout_interface::{LayoutElementType, LayoutNodeType};
|
||||
use servo_arc::Arc;
|
||||
use style::dom::{NodeInfo, OpaqueNode, TNode};
|
||||
use style::dom::{NodeInfo, TNode};
|
||||
use style::properties::ComputedValues;
|
||||
use style::values::computed::Overflow;
|
||||
use style_traits::CSSPixel;
|
||||
|
@ -30,7 +30,7 @@ use crate::fragment_tree::FragmentTree;
|
|||
use crate::geom::{LogicalVec2, PhysicalPoint, PhysicalRect, PhysicalSize};
|
||||
use crate::positioned::{AbsolutelyPositionedBox, PositioningContext};
|
||||
use crate::replaced::ReplacedContents;
|
||||
use crate::style_ext::{ComputedValuesExt, Display, DisplayGeneratingBox, DisplayInside};
|
||||
use crate::style_ext::{Display, DisplayGeneratingBox, DisplayInside};
|
||||
use crate::taffy::{TaffyItemBox, TaffyItemBoxInner};
|
||||
use crate::{DefiniteContainingBlock, PropagatedBoxTreeData};
|
||||
|
||||
|
@ -40,9 +40,6 @@ pub struct BoxTree {
|
|||
/// There may be zero if that element has `display: none`.
|
||||
root: BlockFormattingContext,
|
||||
|
||||
/// <https://drafts.csswg.org/css-backgrounds/#special-backgrounds>
|
||||
canvas_background: CanvasBackground,
|
||||
|
||||
/// Whether or not the viewport should be sensitive to scrolling input events in two axes
|
||||
viewport_scroll_sensitivity: AxesScrollSensitivity,
|
||||
}
|
||||
|
@ -96,7 +93,6 @@ impl BoxTree {
|
|||
contents,
|
||||
contains_floats,
|
||||
},
|
||||
canvas_background: CanvasBackground::for_root_element(context, root_element),
|
||||
// From https://www.w3.org/TR/css-overflow-3/#overflow-propagation:
|
||||
// > If visible is applied to the viewport, it must be interpreted as auto.
|
||||
// > If clip is applied to the viewport, it must be interpreted as hidden.
|
||||
|
@ -425,69 +421,7 @@ impl BoxTree {
|
|||
root_fragments,
|
||||
scrollable_overflow,
|
||||
physical_containing_block,
|
||||
self.canvas_background.clone(),
|
||||
self.viewport_scroll_sensitivity,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// <https://drafts.csswg.org/css-backgrounds/#root-background>
|
||||
#[derive(Clone, MallocSizeOf)]
|
||||
pub struct CanvasBackground {
|
||||
/// DOM node for the root element
|
||||
pub root_element: OpaqueNode,
|
||||
|
||||
/// The element whose style the canvas takes background properties from (see next field).
|
||||
/// This can be the root element (same as the previous field), or the HTML `<body>` element.
|
||||
/// See <https://drafts.csswg.org/css-backgrounds/#body-background>
|
||||
pub from_element: OpaqueNode,
|
||||
|
||||
/// The computed styles to take background properties from.
|
||||
#[conditional_malloc_size_of]
|
||||
pub style: Option<Arc<ComputedValues>>,
|
||||
}
|
||||
|
||||
impl CanvasBackground {
|
||||
fn for_root_element(context: &LayoutContext, root_element: ServoLayoutNode<'_>) -> Self {
|
||||
let root_style = root_element.style(context);
|
||||
|
||||
let mut style = root_style;
|
||||
let mut from_element = root_element;
|
||||
|
||||
// https://drafts.csswg.org/css-backgrounds/#body-background
|
||||
// “if the computed value of background-image on the root element is none
|
||||
// and its background-color is transparent”
|
||||
if style.background_is_transparent() &&
|
||||
// “For documents whose root element is an HTML `HTML` element
|
||||
// or an XHTML `html` element”
|
||||
root_element.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLHtmlElement) &&
|
||||
// Don’t try to access styles for an unstyled subtree
|
||||
!matches!(style.clone_display().into(), Display::None)
|
||||
{
|
||||
// “that element’s first HTML `BODY` or XHTML `body` child element”
|
||||
if let Some(body) = iter_child_nodes(root_element).find(|child| {
|
||||
child.is_element() &&
|
||||
child.type_id() ==
|
||||
LayoutNodeType::Element(LayoutElementType::HTMLBodyElement)
|
||||
}) {
|
||||
style = body.style(context);
|
||||
from_element = body;
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
root_element: root_element.opaque(),
|
||||
from_element: from_element.opaque(),
|
||||
|
||||
// “However, if no boxes are generated for the element
|
||||
// whose background would be used for the canvas
|
||||
// (for example, if the root element has display: none),
|
||||
// then the canvas background is transparent.”
|
||||
style: if let Display::GeneratingBox(_) = style.clone_display().into() {
|
||||
Some(style)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,10 +32,8 @@ impl BaseFragment {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true if this fragment is non-anonymous and it is for the given
|
||||
/// OpaqueNode, regardless of the pseudo element.
|
||||
pub(crate) fn is_for_node(&self, node: OpaqueNode) -> bool {
|
||||
self.tag.map(|tag| tag.node == node).unwrap_or(false)
|
||||
pub(crate) fn is_anonymous(&self) -> bool {
|
||||
self.tag.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use style::logical_geometry::WritingMode;
|
|||
use style::properties::ComputedValues;
|
||||
use style::values::specified::box_::DisplayOutside;
|
||||
|
||||
use super::{BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment};
|
||||
use super::{BaseFragment, BaseFragmentInfo, CollapsedBlockMargins, Fragment, FragmentFlags};
|
||||
use crate::ArcRefCell;
|
||||
use crate::display_list::ToWebRender;
|
||||
use crate::formatting_contexts::Baselines;
|
||||
|
@ -243,6 +243,16 @@ impl BoxFragment {
|
|||
self.margin + self.border + self.padding
|
||||
}
|
||||
|
||||
pub(crate) fn is_root_element(&self) -> bool {
|
||||
self.base.flags.intersects(FragmentFlags::IS_ROOT_ELEMENT)
|
||||
}
|
||||
|
||||
pub(crate) fn is_body_element_of_html_element_root(&self) -> bool {
|
||||
self.base
|
||||
.flags
|
||||
.intersects(FragmentFlags::IS_BODY_ELEMENT_OF_HTML_ELEMENT_ROOT)
|
||||
}
|
||||
|
||||
pub fn print(&self, tree: &mut PrintTree) {
|
||||
tree.new_level(format!(
|
||||
"Box\
|
||||
|
|
|
@ -11,10 +11,10 @@ use malloc_size_of_derive::MallocSizeOf;
|
|||
use style::animation::AnimationSetKey;
|
||||
use webrender_api::units;
|
||||
|
||||
use super::{ContainingBlockManager, Fragment};
|
||||
use super::{BoxFragment, ContainingBlockManager, Fragment};
|
||||
use crate::ArcRefCell;
|
||||
use crate::context::LayoutContext;
|
||||
use crate::display_list::StackingContext;
|
||||
use crate::flow::CanvasBackground;
|
||||
use crate::geom::PhysicalRect;
|
||||
|
||||
#[derive(MallocSizeOf)]
|
||||
|
@ -36,9 +36,6 @@ pub struct FragmentTree {
|
|||
/// The containing block used in the layout of this fragment tree.
|
||||
pub(crate) initial_containing_block: PhysicalRect<Au>,
|
||||
|
||||
/// <https://drafts.csswg.org/css-backgrounds/#special-backgrounds>
|
||||
pub(crate) canvas_background: CanvasBackground,
|
||||
|
||||
/// Whether or not the viewport is sensitive to scroll input events.
|
||||
pub viewport_scroll_sensitivity: AxesScrollSensitivity,
|
||||
}
|
||||
|
@ -49,14 +46,12 @@ impl FragmentTree {
|
|||
root_fragments: Vec<Fragment>,
|
||||
scrollable_overflow: PhysicalRect<Au>,
|
||||
initial_containing_block: PhysicalRect<Au>,
|
||||
canvas_background: CanvasBackground,
|
||||
viewport_scroll_sensitivity: AxesScrollSensitivity,
|
||||
) -> Self {
|
||||
let fragment_tree = Self {
|
||||
root_fragments,
|
||||
scrollable_overflow,
|
||||
initial_containing_block,
|
||||
canvas_background,
|
||||
viewport_scroll_sensitivity,
|
||||
};
|
||||
|
||||
|
@ -102,11 +97,7 @@ impl FragmentTree {
|
|||
root_stacking_context: &StackingContext,
|
||||
) {
|
||||
// Paint the canvas’ background (if any) before/under everything else
|
||||
root_stacking_context.build_canvas_background_display_list(
|
||||
builder,
|
||||
self,
|
||||
&self.initial_containing_block,
|
||||
);
|
||||
root_stacking_context.build_canvas_background_display_list(builder, self);
|
||||
root_stacking_context.build_display_list(builder);
|
||||
}
|
||||
|
||||
|
@ -160,4 +151,45 @@ impl FragmentTree {
|
|||
scroll_area
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the `<body>` element's [`Fragment`], if it exists in this [`FragmentTree`].
|
||||
pub(crate) fn body_fragment(&self) -> Option<ArcRefCell<BoxFragment>> {
|
||||
fn find_body(children: &[Fragment]) -> Option<ArcRefCell<BoxFragment>> {
|
||||
children.iter().find_map(|fragment| {
|
||||
match fragment {
|
||||
Fragment::Box(box_fragment) | Fragment::Float(box_fragment) => {
|
||||
let borrowed_box_fragment = box_fragment.borrow();
|
||||
if borrowed_box_fragment.is_body_element_of_html_element_root() {
|
||||
return Some(box_fragment.clone());
|
||||
}
|
||||
|
||||
// The fragment for the `<body>` element is typically a child of the root (though,
|
||||
// not if it's absolutely positioned), so we need to recurse into the children of
|
||||
// the root to find it.
|
||||
//
|
||||
// Additionally, recurse into any anonymous fragments, as the `<body>` fragment may
|
||||
// have created anonymous parents (for instance by creating an inline formatting context).
|
||||
if borrowed_box_fragment.is_root_element() ||
|
||||
borrowed_box_fragment.base.is_anonymous()
|
||||
{
|
||||
find_body(&borrowed_box_fragment.children)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
Fragment::Positioning(positioning_context)
|
||||
if positioning_context.borrow().base.is_anonymous() =>
|
||||
{
|
||||
// If the `<body>` element is a `display: inline` then it might be nested inside of a
|
||||
// `PositioningFragment` for the purposes of putting it on the first line of the implied
|
||||
// inline formatting context.
|
||||
find_body(&positioning_context.borrow().children)
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
find_body(&self.root_fragments)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[transform-translate-background-001.html]
|
||||
expected: FAIL
|
|
@ -1,2 +0,0 @@
|
|||
[transform-translate-background-002.html]
|
||||
expected: FAIL
|
Loading…
Add table
Add a link
Reference in a new issue