From 99ce81cf64476fb0b36516e8efc5c21c72538fb5 Mon Sep 17 00:00:00 2001 From: Martin Robinson Date: Thu, 14 Aug 2025 15:41:34 +0200 Subject: [PATCH] layout: Support storing layout data for two-level nested pseudo-elements (#38678) Add basic support for storing layout data for pseudo-elements nested to up to two levels. This removes the last unstored layout result and fixes a double-borrow issue. This change does not add properly parsing nor styling of these element types, but does prepare for those changes which must come from stylo. Testing: This fixes a intermittent panic in `tests/wpt/tests/css/css-lists/nested-marker-styling.html` Fixes: #38177. Closes: #38183. Signed-off-by: Martin Robinson Co-authored-by: Oriol Brufau --- components/layout/dom.rs | 171 +++++++++++------- components/layout/dom_traversal.rs | 32 ++-- components/layout/flow/construct.rs | 42 +---- components/layout/flow/root.rs | 4 +- .../layout/fragment_tree/base_fragment.rs | 18 +- .../layout/fragment_tree/fragment_tree.rs | 11 +- components/layout/table/construct.rs | 12 +- components/script/layout_dom/element.rs | 35 ++-- components/script/layout_dom/node.rs | 32 ++-- components/shared/layout/wrapper_traits.rs | 93 ++++++++-- 10 files changed, 273 insertions(+), 177 deletions(-) diff --git a/components/layout/dom.rs b/components/layout/dom.rs index 6578e263538..9c203c052b9 100644 --- a/components/layout/dom.rs +++ b/components/layout/dom.rs @@ -34,7 +34,7 @@ use crate::taffy::TaffyItemBox; #[derive(MallocSizeOf)] pub struct PseudoLayoutData { pseudo: PseudoElement, - box_slot: ArcRefCell>, + data: ArcRefCell, } /// The data that is stored in each DOM node that is used by layout. @@ -45,22 +45,65 @@ pub struct InnerDOMLayoutData { } impl InnerDOMLayoutData { - pub(crate) fn for_pseudo( + fn pseudo_layout_data( &self, - pseudo_element: Option, - ) -> Option>> { - let Some(pseudo_element) = pseudo_element else { - return Some(self.self_box.borrow()); - }; - + pseudo_element: PseudoElement, + ) -> Option> { for pseudo_layout_data in self.pseudo_boxes.iter() { if pseudo_element == pseudo_layout_data.pseudo { - return Some(pseudo_layout_data.box_slot.borrow()); + return Some(pseudo_layout_data.data.clone()); } } - None } + + fn create_pseudo_layout_data( + &mut self, + pseudo_element: PseudoElement, + ) -> ArcRefCell { + let data: ArcRefCell = Default::default(); + self.pseudo_boxes.push(PseudoLayoutData { + pseudo: pseudo_element, + data: data.clone(), + }); + data + } + + fn fragments(&self) -> Vec { + self.self_box + .borrow() + .as_ref() + .map(LayoutBox::fragments) + .unwrap_or_default() + } + + fn repair_style(&self, node: &ServoThreadSafeLayoutNode, context: &SharedStyleContext) { + if let Some(layout_object) = &*self.self_box.borrow() { + layout_object.repair_style(context, node, &node.style(context)); + } + + for pseudo_layout_data in self.pseudo_boxes.iter() { + let Some(node_with_pseudo) = node.with_pseudo(pseudo_layout_data.pseudo) else { + continue; + }; + pseudo_layout_data + .data + .borrow() + .repair_style(&node_with_pseudo, context); + } + } + + fn clear_fragment_layout_cache(&self) { + if let Some(data) = self.self_box.borrow().as_ref() { + data.clear_fragment_layout_cache(); + } + for pseudo_layout_data in self.pseudo_boxes.iter() { + pseudo_layout_data + .data + .borrow() + .clear_fragment_layout_cache(); + } + } } /// A box that is stored in one of the `DOMLayoutData` slots. @@ -191,14 +234,6 @@ impl From>> for BoxSlot<'_> { /// A mutable reference to a `LayoutBox` stored in a DOM element. impl BoxSlot<'_> { - pub(crate) fn dummy() -> Self { - let slot = None; - Self { - slot, - marker: PhantomData, - } - } - pub(crate) fn set(mut self, box_: LayoutBox) { if let Some(slot) = &mut self.slot { *slot.borrow_mut() = Some(box_); @@ -233,10 +268,9 @@ pub(crate) trait NodeExt<'dom> { fn as_svg(&self) -> Option; fn as_typeless_object_with_data_attribute(&self) -> Option; - fn layout_data_mut(&self) -> AtomicRefMut<'dom, InnerDOMLayoutData>; - fn layout_data(&self) -> Option>; - fn element_box_slot(&self) -> BoxSlot<'dom>; - fn pseudo_element_box_slot(&self, pseudo_element: PseudoElement) -> BoxSlot<'dom>; + fn ensure_inner_layout_data(&self) -> AtomicRefMut<'dom, InnerDOMLayoutData>; + fn inner_layout_data(&self) -> Option>; + fn box_slot(&self) -> BoxSlot<'dom>; /// Remove boxes for the element itself, and all of its pseudo-element boxes. fn unset_all_boxes(&self); @@ -327,11 +361,11 @@ impl<'dom> NodeExt<'dom> for ServoThreadSafeLayoutNode<'dom> { .map(|string| string.to_owned()) } - fn layout_data_mut(&self) -> AtomicRefMut<'dom, InnerDOMLayoutData> { - if ThreadSafeLayoutNode::layout_data(self).is_none() { + fn ensure_inner_layout_data(&self) -> AtomicRefMut<'dom, InnerDOMLayoutData> { + if self.layout_data().is_none() { self.initialize_layout_data::(); } - ThreadSafeLayoutNode::layout_data(self) + self.layout_data() .unwrap() .as_any() .downcast_ref::() @@ -340,8 +374,8 @@ impl<'dom> NodeExt<'dom> for ServoThreadSafeLayoutNode<'dom> { .borrow_mut() } - fn layout_data(&self) -> Option> { - ThreadSafeLayoutNode::layout_data(self).map(|data| { + fn inner_layout_data(&self) -> Option> { + self.layout_data().map(|data| { data.as_any() .downcast_ref::() .unwrap() @@ -350,22 +384,38 @@ impl<'dom> NodeExt<'dom> for ServoThreadSafeLayoutNode<'dom> { }) } - fn element_box_slot(&self) -> BoxSlot<'dom> { - self.layout_data_mut().self_box.clone().into() - } + fn box_slot(&self) -> BoxSlot<'dom> { + let pseudo_element_chain = self.pseudo_element_chain(); + let Some(primary) = pseudo_element_chain.primary else { + return self.ensure_inner_layout_data().self_box.clone().into(); + }; - fn pseudo_element_box_slot(&self, pseudo_element: PseudoElement) -> BoxSlot<'dom> { - let mut layout_data = self.layout_data_mut(); - let box_slot = ArcRefCell::new(None); - layout_data.pseudo_boxes.push(PseudoLayoutData { - pseudo: pseudo_element, - box_slot: box_slot.clone(), - }); - box_slot.into() + let Some(secondary) = pseudo_element_chain.secondary else { + let primary_layout_data = self + .ensure_inner_layout_data() + .create_pseudo_layout_data(primary); + return primary_layout_data.borrow().self_box.clone().into(); + }; + + // It's *very* important that this not borrow the element's main + // `InnerLayoutData`. Primary pseudo-elements are processed at the same recursion + // level as the main data, so the `BoxSlot` is created sequentially with other + // primary pseudo-elements and the element itself. The secondary pseudo-element is + // one level deep, so could be happening in parallel with the primary + // pseudo-elements or main element layout. + let primary_layout_data = self + .inner_layout_data() + .expect("Should already have element InnerLayoutData here.") + .pseudo_layout_data(primary) + .expect("Should already have primary pseudo-element InnerLayoutData here"); + let secondary_layout_data = primary_layout_data + .borrow_mut() + .create_pseudo_layout_data(secondary); + secondary_layout_data.borrow().self_box.clone().into() } fn unset_all_boxes(&self) { - let mut layout_data = self.layout_data_mut(); + let mut layout_data = self.ensure_inner_layout_data(); *layout_data.self_box.borrow_mut() = None; layout_data.pseudo_boxes.clear(); @@ -374,48 +424,31 @@ impl<'dom> NodeExt<'dom> for ServoThreadSafeLayoutNode<'dom> { } fn unset_all_pseudo_boxes(&self) { - self.layout_data_mut().pseudo_boxes.clear(); + self.ensure_inner_layout_data().pseudo_boxes.clear(); } fn clear_fragment_layout_cache(&self) { - let data = self.layout_data_mut(); - if let Some(data) = data.self_box.borrow_mut().as_ref() { - data.clear_fragment_layout_cache(); - } - - for pseudo_layout_data in data.pseudo_boxes.iter() { - if let Some(layout_box) = pseudo_layout_data.box_slot.borrow().as_ref() { - layout_box.clear_fragment_layout_cache(); - } + if let Some(inner_layout_data) = self.inner_layout_data() { + inner_layout_data.clear_fragment_layout_cache(); } } fn fragments_for_pseudo(&self, pseudo_element: Option) -> Vec { - let Some(layout_data) = NodeExt::layout_data(self) else { + let Some(layout_data) = self.inner_layout_data() else { return vec![]; }; - let Some(layout_data) = layout_data.for_pseudo(pseudo_element) else { - return vec![]; - }; - layout_data - .as_ref() - .map(LayoutBox::fragments) - .unwrap_or_default() + match pseudo_element { + Some(pseudo_element) => layout_data + .pseudo_layout_data(pseudo_element) + .map(|pseudo_layout_data| pseudo_layout_data.borrow().fragments()) + .unwrap_or_default(), + None => layout_data.fragments(), + } } fn repair_style(&self, context: &SharedStyleContext) { - let data = self.layout_data_mut(); - if let Some(layout_object) = &*data.self_box.borrow() { - let style = self.style(context); - layout_object.repair_style(context, self, &style); - } - - for pseudo_layout_data in data.pseudo_boxes.iter() { - if let Some(layout_box) = pseudo_layout_data.box_slot.borrow().as_ref() { - if let Some(node) = self.with_pseudo(pseudo_layout_data.pseudo) { - layout_box.repair_style(context, self, &node.style(context)); - } - } + if let Some(layout_data) = self.inner_layout_data() { + layout_data.repair_style(self, context); } } diff --git a/components/layout/dom_traversal.rs b/components/layout/dom_traversal.rs index cacc1770b2e..e817e626ac5 100644 --- a/components/layout/dom_traversal.rs +++ b/components/layout/dom_traversal.rs @@ -6,7 +6,9 @@ use std::borrow::Cow; use fonts::ByteIndex; use html5ever::{LocalName, local_name}; -use layout_api::wrapper_traits::{ThreadSafeLayoutElement, ThreadSafeLayoutNode}; +use layout_api::wrapper_traits::{ + PseudoElementChain, ThreadSafeLayoutElement, ThreadSafeLayoutNode, +}; use layout_api::{LayoutDamage, LayoutElementType, LayoutNodeType}; use range::Range; use script::layout_dom::ServoThreadSafeLayoutNode; @@ -48,8 +50,8 @@ impl<'dom> NodeAndStyleInfo<'dom> { } } - pub(crate) fn pseudo_element(&self) -> Option { - self.node.pseudo_element() + pub(crate) fn pseudo_element_chain(&self) -> PseudoElementChain { + self.node.pseudo_element_chain() } pub(crate) fn with_pseudo_element( @@ -74,7 +76,7 @@ impl<'dom> NodeAndStyleInfo<'dom> { impl<'dom> From<&NodeAndStyleInfo<'dom>> for BaseFragmentInfo { fn from(info: &NodeAndStyleInfo<'dom>) -> Self { let threadsafe_node = info.node; - let pseudo = info.node.pseudo_element(); + let pseudo_element_chain = info.node.pseudo_element_chain(); let mut flags = FragmentFlags::empty(); // Anonymous boxes should not have a tag, because they should not take part in hit testing. @@ -83,7 +85,7 @@ impl<'dom> From<&NodeAndStyleInfo<'dom>> for BaseFragmentInfo { // cases, but currently this means that the order of hit test results isn't as expected for // some WPT tests. This needs more investigation. if matches!( - pseudo, + pseudo_element_chain.innermost(), Some(PseudoElement::ServoAnonymousBox) | Some(PseudoElement::ServoAnonymousTable) | Some(PseudoElement::ServoAnonymousTableCell) | @@ -122,7 +124,10 @@ impl<'dom> From<&NodeAndStyleInfo<'dom>> for BaseFragmentInfo { }; Self { - tag: Some(Tag::new_pseudo(threadsafe_node.opaque(), pseudo)), + tag: Some(Tag::new_pseudo( + threadsafe_node.opaque(), + pseudo_element_chain, + )), flags, } } @@ -232,7 +237,7 @@ fn traverse_element<'dom>( } else { let shared_inline_styles: SharedInlineStyles = (&info).into(); element - .element_box_slot() + .box_slot() .set(LayoutBox::DisplayContents(shared_inline_styles.clone())); handler.enter_display_contents(shared_inline_styles); @@ -254,7 +259,7 @@ fn traverse_element<'dom>( NonReplacedContents::OfElement.into() }; let display = display.used_value_for_contents(&contents); - let box_slot = element.element_box_slot(); + let box_slot = element.box_slot(); handler.handle_element(&info, display, contents, box_slot); }, } @@ -282,9 +287,7 @@ fn traverse_eager_pseudo_element<'dom>( Display::None => {}, Display::Contents => { let items = generate_pseudo_element_content(&pseudo_element_info, context); - let box_slot = pseudo_element_info - .node - .pseudo_element_box_slot(pseudo_element_type); + let box_slot = pseudo_element_info.node.box_slot(); let shared_inline_styles: SharedInlineStyles = (&pseudo_element_info).into(); box_slot.set(LayoutBox::DisplayContents(shared_inline_styles.clone())); @@ -294,9 +297,7 @@ fn traverse_eager_pseudo_element<'dom>( }, Display::GeneratingBox(display) => { let items = generate_pseudo_element_content(&pseudo_element_info, context); - let box_slot = pseudo_element_info - .node - .pseudo_element_box_slot(pseudo_element_type); + let box_slot = pseudo_element_info.node.box_slot(); let contents = NonReplacedContents::OfPseudoElement(items).into(); handler.handle_element(&pseudo_element_info, display, contents, box_slot); }, @@ -333,8 +334,7 @@ fn traverse_pseudo_element_contents<'dom>( anonymous_info, display_inline, Contents::Replaced(contents), - info.node - .pseudo_element_box_slot(PseudoElement::ServoAnonymousBox), + anonymous_info.node.box_slot(), ) }, } diff --git a/components/layout/flow/construct.rs b/components/layout/flow/construct.rs index 19ee0ed76cf..5ee676bd91b 100644 --- a/components/layout/flow/construct.rs +++ b/components/layout/flow/construct.rs @@ -171,11 +171,10 @@ impl BlockContainer { if let Some((marker_info, marker_contents)) = crate::lists::make_marker(context, info) { match marker_info.style.clone_list_style_position() { ListStylePosition::Inside => { - builder.handle_list_item_marker_inside(&marker_info, info, marker_contents) + builder.handle_list_item_marker_inside(&marker_info, marker_contents) }, ListStylePosition::Outside => builder.handle_list_item_marker_outside( &marker_info, - info, marker_contents, info.style.clone(), ), @@ -299,9 +298,7 @@ impl<'dom, 'style> BlockContainerBuilder<'dom, 'style> { self.push_block_level_job_for_inline_formatting_context(inline_formatting_context); } - let box_slot = table_info - .node - .pseudo_element_box_slot(PseudoElement::ServoAnonymousTable); + let box_slot = table_info.node.box_slot(); self.block_level_boxes.push(BlockLevelJob { info: table_info, box_slot, @@ -408,19 +405,9 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { fn handle_list_item_marker_inside( &mut self, marker_info: &NodeAndStyleInfo<'dom>, - container_info: &NodeAndStyleInfo<'dom>, contents: Vec, ) { - // TODO: We do not currently support saving box slots for ::marker pseudo-elements - // that are part nested in ::before and ::after pseudo elements. For now, just - // forget about them once they are built. - let box_slot = match container_info.pseudo_element() { - Some(_) => BoxSlot::dummy(), - None => marker_info - .node - .pseudo_element_box_slot(PseudoElement::Marker), - }; - + let box_slot = marker_info.node.box_slot(); self.handle_inline_level_element( marker_info, DisplayInside::Flow { @@ -434,20 +421,10 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { fn handle_list_item_marker_outside( &mut self, marker_info: &NodeAndStyleInfo<'dom>, - container_info: &NodeAndStyleInfo<'dom>, contents: Vec, list_item_style: Arc, ) { - // TODO: We do not currently support saving box slots for ::marker pseudo-elements - // that are part nested in ::before and ::after pseudo elements. For now, just - // forget about them once they are built. - let box_slot = match container_info.pseudo_element() { - Some(_) => BoxSlot::dummy(), - None => marker_info - .node - .pseudo_element_box_slot(PseudoElement::Marker), - }; - + let box_slot = marker_info.node.box_slot(); self.block_level_boxes.push(BlockLevelJob { info: marker_info.clone(), box_slot, @@ -511,7 +488,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { // Ignore `list-style-position` here: // “If the list item is an inline box: this value is equivalent to `inside`.” // https://drafts.csswg.org/css-lists/#list-style-position-outside - self.handle_list_item_marker_inside(&marker_info, info, marker_contents) + self.handle_list_item_marker_inside(&marker_info, marker_contents) } } @@ -682,7 +659,7 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { inline_formatting_context: InlineFormattingContext, ) { let layout_context = self.context; - let info = self + let anonymous_info = self .anonymous_box_info .get_or_insert_with(|| { self.info @@ -691,12 +668,9 @@ impl<'dom> BlockContainerBuilder<'dom, '_> { }) .clone(); - let box_slot = self - .info - .node - .pseudo_element_box_slot(PseudoElement::ServoAnonymousBox); + let box_slot = anonymous_info.node.box_slot(); self.block_level_boxes.push(BlockLevelJob { - info, + info: anonymous_info, box_slot, kind: BlockLevelCreator::SameFormattingContextBlock( IntermediateBlockContainer::InlineFormattingContext( diff --git a/components/layout/flow/root.rs b/components/layout/flow/root.rs index 5ce6b5e153c..fb357de3c84 100644 --- a/components/layout/flow/root.rs +++ b/components/layout/flow/root.rs @@ -190,7 +190,7 @@ fn construct_for_root_element( let root_box = ArcRefCell::new(root_box); root_element - .element_box_slot() + .box_slot() .set(LayoutBox::BlockLevel(root_box.clone())); vec![root_box] } @@ -301,7 +301,7 @@ impl<'dom> IncrementalBoxTreeUpdate<'dom> { return None; } - let layout_data = NodeExt::layout_data(&potential_thread_safe_dirty_root_node)?; + let layout_data = NodeExt::inner_layout_data(&potential_thread_safe_dirty_root_node)?; if !layout_data.pseudo_boxes.is_empty() { return None; } diff --git a/components/layout/fragment_tree/base_fragment.rs b/components/layout/fragment_tree/base_fragment.rs index 8009135c17d..326a582b058 100644 --- a/components/layout/fragment_tree/base_fragment.rs +++ b/components/layout/fragment_tree/base_fragment.rs @@ -4,10 +4,10 @@ use bitflags::bitflags; use layout_api::combine_id_with_fragment_type; +use layout_api::wrapper_traits::PseudoElementChain; use malloc_size_of::malloc_size_of_is_0; use malloc_size_of_derive::MallocSizeOf; use style::dom::OpaqueNode; -use style::selector_parser::PseudoElement; /// This data structure stores fields that are common to all non-base /// Fragment types and should generally be the first member of all @@ -114,23 +114,29 @@ malloc_size_of_is_0!(FragmentFlags); #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] pub(crate) struct Tag { pub(crate) node: OpaqueNode, - pub(crate) pseudo: Option, + pub(crate) pseudo_element_chain: PseudoElementChain, } impl Tag { /// Create a new Tag for a non-pseudo element. This is mainly used for /// matching existing tags, since it does not accept an `info` argument. pub(crate) fn new(node: OpaqueNode) -> Self { - Tag { node, pseudo: None } + Tag { + node, + pseudo_element_chain: Default::default(), + } } /// Create a new Tag for a pseudo element. This is mainly used for /// matching existing tags, since it does not accept an `info` argument. - pub(crate) fn new_pseudo(node: OpaqueNode, pseudo: Option) -> Self { - Tag { node, pseudo } + pub(crate) fn new_pseudo(node: OpaqueNode, pseudo_element_chain: PseudoElementChain) -> Self { + Tag { + node, + pseudo_element_chain, + } } pub(crate) fn to_display_list_fragment_id(self) -> u64 { - combine_id_with_fragment_type(self.node.id(), self.pseudo.into()) + combine_id_with_fragment_type(self.node.id(), self.pseudo_element_chain.primary.into()) } } diff --git a/components/layout/fragment_tree/fragment_tree.rs b/components/layout/fragment_tree/fragment_tree.rs index 69c1e404c33..4b6e207fcc1 100644 --- a/components/layout/fragment_tree/fragment_tree.rs +++ b/components/layout/fragment_tree/fragment_tree.rs @@ -71,8 +71,15 @@ impl FragmentTree { fragment_tree.find(|fragment, _level, containing_block| { if let Some(tag) = fragment.tag() { - invalid_animating_nodes.remove(&AnimationSetKey::new(tag.node, tag.pseudo)); - invalid_image_animating_nodes.remove(&AnimationSetKey::new(tag.node, tag.pseudo)); + // TODO: Support animations on nested pseudo-elements. + invalid_animating_nodes.remove(&AnimationSetKey::new( + tag.node, + tag.pseudo_element_chain.primary, + )); + invalid_image_animating_nodes.remove(&AnimationSetKey::new( + tag.node, + tag.pseudo_element_chain.primary, + )); } fragment.set_containing_block(containing_block); diff --git a/components/layout/table/construct.rs b/components/layout/table/construct.rs index 1bf6d475178..55dbf0d5e95 100644 --- a/components/layout/table/construct.rs +++ b/components/layout/table/construct.rs @@ -720,9 +720,9 @@ impl<'style, 'dom> TableBuilderTraversal<'style, 'dom> { }); self.push_table_row(table_row.clone()); - self.info + anonymous_info .node - .pseudo_element_box_slot(PseudoElement::ServoAnonymousTableRow) + .box_slot() .set(LayoutBox::TableLevelBox(TableLevelBox::Track(table_row))) } @@ -989,9 +989,9 @@ impl<'style, 'builder, 'dom, 'a> TableRowBuilder<'style, 'builder, 'dom, 'a> { .builder .add_cell(new_table_cell.clone()); - self.info + anonymous_info .node - .pseudo_element_box_slot(PseudoElement::ServoAnonymousTableCell) + .box_slot() .set(LayoutBox::TableLevelBox(TableLevelBox::Cell( new_table_cell, ))); @@ -1027,7 +1027,7 @@ impl<'dom> TraversalHandler<'dom> for TableRowBuilder<'_, '_, 'dom, '_> { let cell = old_cell.unwrap_or_else(|| { // This value will already have filtered out rowspan=0 // in quirks mode, so we don't have to worry about that. - let (rowspan, colspan) = if info.pseudo_element().is_none() { + let (rowspan, colspan) = if info.pseudo_element_chain().is_empty() { let rowspan = info.node.get_rowspan().unwrap_or(1) as usize; let colspan = info.node.get_colspan().unwrap_or(1) as usize; @@ -1148,7 +1148,7 @@ fn add_column( is_anonymous: bool, old_column: Option>, ) -> ArcRefCell { - let span = if column_info.pseudo_element().is_none() { + let span = if column_info.pseudo_element_chain().is_empty() { column_info.node.get_span().unwrap_or(1) } else { 1 diff --git a/components/script/layout_dom/element.rs b/components/script/layout_dom/element.rs index 73e4609a9e7..1ab90d40aa0 100644 --- a/components/script/layout_dom/element.rs +++ b/components/script/layout_dom/element.rs @@ -10,7 +10,9 @@ use atomic_refcell::{AtomicRef, AtomicRefMut}; use embedder_traits::UntrustedNodeAddress; use html5ever::{LocalName, Namespace, local_name, ns}; use js::jsapi::JSObject; -use layout_api::wrapper_traits::{LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode}; +use layout_api::wrapper_traits::{ + LayoutNode, PseudoElementChain, ThreadSafeLayoutElement, ThreadSafeLayoutNode, +}; use layout_api::{LayoutDamage, LayoutNodeType, StyleData}; use selectors::Element as _; use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; @@ -967,11 +969,11 @@ impl<'dom> ::selectors::Element for ServoLayoutElement<'dom> { /// ever access safe properties and cannot race on elements. #[derive(Clone, Copy, Debug)] pub struct ServoThreadSafeLayoutElement<'dom> { + /// The wrapped [`ServoLayoutNode`]. pub(super) element: ServoLayoutElement<'dom>, - /// The pseudo-element type for this element, or `None` if it is the non-pseudo - /// version of the element. - pub(super) pseudo: Option, + /// The possibly nested [`PseudoElementChain`] for this node. + pub(super) pseudo_element_chain: PseudoElementChain, } impl<'dom> ServoThreadSafeLayoutElement<'dom> { @@ -995,32 +997,32 @@ impl<'dom> ThreadSafeLayoutElement<'dom> for ServoThreadSafeLayoutElement<'dom> fn as_node(&self) -> ServoThreadSafeLayoutNode<'dom> { ServoThreadSafeLayoutNode { node: self.element.as_node(), - pseudo: self.pseudo, + pseudo_element_chain: self.pseudo_element_chain, } } - fn pseudo_element(&self) -> Option { - self.pseudo + fn pseudo_element_chain(&self) -> PseudoElementChain { + self.pseudo_element_chain } - fn with_pseudo(&self, pseudo_element_type: PseudoElement) -> Option { - if pseudo_element_type.is_eager() && + fn with_pseudo(&self, pseudo_element: PseudoElement) -> Option { + if pseudo_element.is_eager() && self.style_data() .styles .pseudos - .get(&pseudo_element_type) + .get(&pseudo_element) .is_none() { return None; } - if pseudo_element_type == PseudoElement::DetailsSummary && + if pseudo_element == PseudoElement::DetailsSummary && (!self.has_local_name(&local_name!("details")) || !self.has_namespace(&ns!(html))) { return None; } - if pseudo_element_type == PseudoElement::DetailsContent && + if pseudo_element == PseudoElement::DetailsContent && (!self.has_local_name(&local_name!("details")) || !self.has_namespace(&ns!(html)) || self.get_attr(&ns!(), &local_name!("open")).is_none()) @@ -1028,9 +1030,16 @@ impl<'dom> ThreadSafeLayoutElement<'dom> for ServoThreadSafeLayoutElement<'dom> return None; } + // These pseudo-element type cannot be nested. + if !self.pseudo_element_chain.is_empty() { + assert!(!pseudo_element.is_eager()); + assert!(pseudo_element != PseudoElement::DetailsSummary); + assert!(pseudo_element != PseudoElement::DetailsContent); + } + Some(ServoThreadSafeLayoutElement { element: self.element, - pseudo: Some(pseudo_element_type), + pseudo_element_chain: self.pseudo_element_chain.with_pseudo(pseudo_element), }) } diff --git a/components/script/layout_dom/node.rs b/components/script/layout_dom/node.rs index fb38e3e9234..72b4cf5b432 100644 --- a/components/script/layout_dom/node.rs +++ b/components/script/layout_dom/node.rs @@ -11,7 +11,7 @@ use std::iter::FusedIterator; use base::id::{BrowsingContextId, PipelineId}; use fonts_traits::ByteIndex; use layout_api::wrapper_traits::{ - LayoutDataTrait, LayoutNode, ThreadSafeLayoutElement, ThreadSafeLayoutNode, + LayoutDataTrait, LayoutNode, PseudoElementChain, ThreadSafeLayoutElement, ThreadSafeLayoutNode, }; use layout_api::{ GenericLayoutData, HTMLCanvasData, HTMLMediaData, LayoutElementType, LayoutNodeType, @@ -218,18 +218,20 @@ impl<'dom> LayoutNode<'dom> for ServoLayoutNode<'dom> { /// never access any other node apart from its parent. #[derive(Clone, Copy, Debug, PartialEq)] pub struct ServoThreadSafeLayoutNode<'dom> { - /// The wrapped `ServoLayoutNode`. + /// The wrapped [`ServoLayoutNode`]. pub(super) node: ServoLayoutNode<'dom>, - /// The pseudo-element type for this node, or `None` if it is the non-pseudo - /// version of the element. - pub(super) pseudo: Option, + /// The possibly nested [`PseudoElementChain`] for this node. + pub(super) pseudo_element_chain: PseudoElementChain, } impl<'dom> ServoThreadSafeLayoutNode<'dom> { /// Creates a new `ServoThreadSafeLayoutNode` from the given `ServoLayoutNode`. pub fn new(node: ServoLayoutNode<'dom>) -> Self { - ServoThreadSafeLayoutNode { node, pseudo: None } + ServoThreadSafeLayoutNode { + node, + pseudo_element_chain: Default::default(), + } } /// Returns the interior of this node as a `LayoutDom`. This is highly unsafe for layout to @@ -267,7 +269,8 @@ impl<'dom> ServoThreadSafeLayoutNode<'dom> { // rendering it as a bare html element. pub fn is_single_line_text_input(&self) -> bool { self.type_id() == Some(LayoutNodeType::Element(LayoutElementType::HTMLInputElement)) || - (self.pseudo.is_none() && self.node.node.is_text_container_of_single_line_input()) + (self.pseudo_element_chain.is_empty() && + self.node.node.is_text_container_of_single_line_input()) } pub fn is_text_input(&self) -> bool { @@ -316,12 +319,12 @@ impl<'dom> ThreadSafeLayoutNode<'dom> for ServoThreadSafeLayoutNode<'dom> { unsafe { self.get_jsmanaged().opaque() } } - fn pseudo_element(&self) -> Option { - self.pseudo + fn pseudo_element_chain(&self) -> PseudoElementChain { + self.pseudo_element_chain } fn type_id(&self) -> Option { - if self.pseudo.is_none() { + if self.pseudo_element_chain.is_empty() { Some(self.node.type_id()) } else { None @@ -356,7 +359,7 @@ impl<'dom> ThreadSafeLayoutNode<'dom> for ServoThreadSafeLayoutNode<'dom> { .as_element() .map(|el| ServoThreadSafeLayoutElement { element: el, - pseudo: self.pseudo, + pseudo_element_chain: self.pseudo_element_chain, }) } @@ -460,6 +463,13 @@ impl<'dom> ThreadSafeLayoutNode<'dom> for ServoThreadSafeLayoutNode<'dom> { .get_rowspan() } } + + fn with_pseudo_element_chain(&self, pseudo_element_chain: PseudoElementChain) -> Self { + Self { + node: self.node, + pseudo_element_chain, + } + } } pub enum ServoThreadSafeLayoutNodeChildrenIterator<'dom> { diff --git a/components/shared/layout/wrapper_traits.rs b/components/shared/layout/wrapper_traits.rs index 31016859f0d..d8a0fe0d6bc 100644 --- a/components/shared/layout/wrapper_traits.rs +++ b/components/shared/layout/wrapper_traits.rs @@ -11,6 +11,7 @@ use atomic_refcell::AtomicRef; use base::id::{BrowsingContextId, PipelineId}; use fonts_traits::ByteIndex; use html5ever::{LocalName, Namespace}; +use malloc_size_of_derive::MallocSizeOf; use net_traits::image_cache::Image; use pixels::ImageMetadata; use range::Range; @@ -25,8 +26,8 @@ use style::selector_parser::{PseudoElement, PseudoElementCascadeType, SelectorIm use style::stylist::RuleInclusion; use crate::{ - FragmentType, GenericLayoutData, GenericLayoutDataTrait, HTMLCanvasData, HTMLMediaData, - LayoutNodeType, SVGElementData, StyleData, + GenericLayoutData, GenericLayoutDataTrait, HTMLCanvasData, HTMLMediaData, LayoutNodeType, + SVGElementData, StyleData, }; pub trait LayoutDataTrait: GenericLayoutDataTrait + Default + Send + Sync + 'static {} @@ -232,11 +233,7 @@ pub trait ThreadSafeLayoutNode<'dom>: Clone + Copy + Debug + NodeInfo + PartialE fn get_colspan(&self) -> Option; fn get_rowspan(&self) -> Option; - fn pseudo_element(&self) -> Option; - - fn fragment_type(&self) -> FragmentType { - self.pseudo_element().into() - } + fn pseudo_element_chain(&self) -> PseudoElementChain; fn with_pseudo(&self, pseudo_element_type: PseudoElement) -> Option { self.as_element() @@ -244,6 +241,8 @@ pub trait ThreadSafeLayoutNode<'dom>: Clone + Copy + Debug + NodeInfo + PartialE .as_ref() .map(ThreadSafeLayoutElement::as_node) } + + fn with_pseudo_element_chain(&self, pseudo_element_chain: PseudoElementChain) -> Self; } pub trait ThreadSafeLayoutElement<'dom>: @@ -294,7 +293,7 @@ pub trait ThreadSafeLayoutElement<'dom>: fn style_data(&self) -> AtomicRef; - fn pseudo_element(&self) -> Option; + fn pseudo_element_chain(&self) -> PseudoElementChain; /// Returns the style results for the given node. If CSS selector matching /// has not yet been performed, fails. @@ -302,27 +301,25 @@ pub trait ThreadSafeLayoutElement<'dom>: /// Unlike the version on TNode, this handles pseudo-elements. #[inline] fn style(&self, context: &SharedStyleContext) -> Arc { - let data = self.style_data(); - match self.pseudo_element() { - None => data.styles.primary().clone(), - Some(style_pseudo) => { + let get_style_for_pseudo_element = + |base_style: &Arc, pseudo_element: PseudoElement| { // Precompute non-eagerly-cascaded pseudo-element styles if not // cached before. - match style_pseudo.cascade_type() { + match pseudo_element.cascade_type() { // Already computed during the cascade. PseudoElementCascadeType::Eager => self .style_data() .styles .pseudos - .get(&style_pseudo) + .get(&pseudo_element) .unwrap() .clone(), PseudoElementCascadeType::Precomputed => context .stylist .precomputed_values_for_pseudo::( &context.guards, - &style_pseudo, - Some(data.styles.primary()), + &pseudo_element, + Some(base_style), ), PseudoElementCascadeType::Lazy => { context @@ -330,16 +327,30 @@ pub trait ThreadSafeLayoutElement<'dom>: .lazily_compute_pseudo_element_style( &context.guards, self.unsafe_get(), - &style_pseudo, + &pseudo_element, RuleInclusion::All, - data.styles.primary(), + base_style, /* is_probe = */ false, /* matching_func = */ None, ) .unwrap() }, } + }; + + let data = self.style_data(); + let element_style = data.styles.primary(); + let pseudo_element_chain = self.pseudo_element_chain(); + + let primary_pseudo_style = match pseudo_element_chain.primary { + Some(pseudo_element) => get_style_for_pseudo_element(element_style, pseudo_element), + None => return element_style.clone(), + }; + match pseudo_element_chain.secondary { + Some(pseudo_element) => { + get_style_for_pseudo_element(&primary_pseudo_style, pseudo_element) }, + None => primary_pseudo_style, } } @@ -361,3 +372,49 @@ pub trait ThreadSafeLayoutElement<'dom>: /// As in that case, since this is an immutable borrow, we do not violate thread safety. fn is_root(&self) -> bool; } + +/// A chain of pseudo-elements up to two levels deep. This is used to represent cases +/// where a pseudo-element has its own child pseudo element (for instance +/// `.div::after::marker`). If both [`Self::primary`] and [`Self::secondary`] are `None`, +/// then this chain represents the element itself. Not all combinations of pseudo-elements +/// are possible and we may not be able to calculate a style for all +/// [`PseudoElementChain`]s. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, MallocSizeOf, PartialEq)] +pub struct PseudoElementChain { + pub primary: Option, + pub secondary: Option, +} + +impl PseudoElementChain { + pub fn unnested(pseudo_element: PseudoElement) -> Self { + Self { + primary: Some(pseudo_element), + secondary: None, + } + } + + pub fn innermost(&self) -> Option { + self.secondary.or(self.primary) + } + + /// Return a possibly nested [`PseudoElementChain`]. Currently only `::before` and + /// `::after` only support nesting. If the primary [`PseudoElement`] on the chain is + /// not `::before` or `::after` a single element chain is returned for the given + /// [`PseudoElement`]. + pub fn with_pseudo(&self, pseudo_element: PseudoElement) -> Self { + match self.primary { + Some(primary) if primary.is_before_or_after() => Self { + primary: self.primary, + secondary: Some(pseudo_element), + }, + _ => { + assert!(self.secondary.is_none()); + Self::unnested(pseudo_element) + }, + } + } + + pub fn is_empty(&self) -> bool { + self.primary.is_none() + } +}