Files
servo/components/script/layout_dom/element.rs
Martin Robinson 042d95220a layout: Replace dirty root approach with flexible box tree layout (#42700)
Replace the old dirty root box tree layout approach with one that works
based on independent formatting contexts. Instead of just allowing a
single dirty root to be the source of box tree reconstruction, allow box
tree reconstruction to happen anywhere in the tree that can isolate box
tree damage from ancestors. This essentially combines damage propagation
and box tree construction into a single step.

There is currently one downside to this approach compared to the dirty
root approach which is that we currently cannot detect and start box
tree layout when the dirty has a compatible `display` and `position`
value. This can mean the scope of box tree layout extends further up the
tree. We will address this in a followup -- but have not noticed any
major performance implications (currently fragment tree layout is much
more expensive than box tree layout).

Benefits:
 1. Damage propagation now only happens under the dirty root.
 2. Future changes can limit the scope of damage up the tree and perhaps
    preserve the inline content size cache between box tree rebuilds.

Testing: This should not change behavior in a testable way (we currently
do
not have robust performance tests), so WPT test results should not
change.

---------

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
Co-authored-by: Luke Warlow <lwarlow@igalia.com>
2026-02-19 14:31:13 +00:00

1322 lines
44 KiB
Rust

/* 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::hash::Hash;
use std::sync::atomic::Ordering;
use std::{fmt, slice};
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, PseudoElementChain, ThreadSafeLayoutElement, ThreadSafeLayoutNode,
};
use layout_api::{LayoutDamage, LayoutNodeType, StyleData};
use selectors::Element as _;
use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
use selectors::bloom::{BLOOM_HASH_MASK, BloomFilter};
use selectors::matching::{ElementSelectorFlags, MatchingContext, VisitedHandlingMode};
use selectors::sink::Push;
use servo_arc::{Arc, ArcBorrow};
use style::CaseSensitivityExt;
use style::animation::AnimationSetKey;
use style::applicable_declarations::ApplicableDeclarationBlock;
use style::attr::AttrValue;
use style::bloom::each_relevant_element_hash;
use style::context::SharedStyleContext;
use style::data::ElementData;
use style::dom::{DomChildren, LayoutIterator, TDocument, TElement, TNode, TShadowRoot};
use style::properties::{ComputedValues, PropertyDeclarationBlock};
use style::selector_parser::{
AttrValue as SelectorAttrValue, Lang, NonTSPseudoClass, PseudoElement, RestyleDamage,
SelectorImpl, extended_filtering,
};
use style::shared_lock::Locked as StyleLocked;
use style::stylesheets::scope_rule::ImplicitScopeRoot;
use style::values::computed::{Display, Image};
use style::values::generics::counters::{Content, ContentItem, GenericContentItems};
use style::values::specified::align::AlignFlags;
use style::values::specified::box_::{DisplayInside, DisplayOutside};
use style::values::{AtomIdent, AtomString};
use stylo_atoms::Atom;
use stylo_dom::ElementState;
use crate::dom::attr::AttrHelpersForLayout;
use crate::dom::bindings::inheritance::{
Castable, CharacterDataTypeId, DocumentFragmentTypeId, ElementTypeId, HTMLElementTypeId,
NodeTypeId, TextTypeId,
};
use crate::dom::bindings::root::LayoutDom;
use crate::dom::characterdata::LayoutCharacterDataHelpers;
use crate::dom::element::{Element, LayoutElementHelpers};
use crate::dom::html::htmlslotelement::HTMLSlotElement;
use crate::dom::node::{LayoutNodeHelpers, Node, NodeFlags};
use crate::layout_dom::{ServoLayoutNode, ServoShadowRoot, ServoThreadSafeLayoutNode};
/// A wrapper around elements that ensures layout can only ever access safe properties.
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub struct ServoLayoutElement<'dom> {
/// The wrapped private DOM Element.
element: LayoutDom<'dom, Element>,
}
impl fmt::Debug for ServoLayoutElement<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<{}", self.element.local_name())?;
if let Some(id) = self.id() {
write!(f, " id={}", id)?;
}
write!(f, "> ({:#x})", self.as_node().opaque().0)
}
}
impl<'dom> ServoLayoutElement<'dom> {
pub(super) fn from_layout_js(el: LayoutDom<'dom, Element>) -> Self {
ServoLayoutElement { element: el }
}
pub(super) fn is_html_element(&self) -> bool {
self.element.is_html_element()
}
#[inline]
fn get_attr_enum(&self, namespace: &Namespace, name: &LocalName) -> Option<&AttrValue> {
self.element.get_attr_for_layout(namespace, name)
}
#[inline]
fn get_attr(&self, namespace: &Namespace, name: &LocalName) -> Option<&str> {
self.element.get_attr_val_for_layout(namespace, name)
}
fn get_style_data(&self) -> Option<&StyleData> {
self.as_node().style_data()
}
/// Unset the snapshot flags on the underlying DOM object for this element.
///
/// # Safety
///
/// This function accesses and modifies the underlying DOM object and should
/// not be used by more than a single thread at once.
pub unsafe fn unset_snapshot_flags(&self) {
unsafe {
self.as_node()
.node
.set_flag(NodeFlags::HAS_SNAPSHOT | NodeFlags::HANDLED_SNAPSHOT, false);
}
}
/// Unset the snapshot flags on the underlying DOM object for this element.
///
/// # Safety
///
/// This function accesses and modifies the underlying DOM object and should
/// not be used by more than a single thread at once.
pub unsafe fn set_has_snapshot(&self) {
unsafe {
self.as_node().node.set_flag(NodeFlags::HAS_SNAPSHOT, true);
}
}
/// Returns true if this element is the body child of an html element root element.
fn is_body_element_of_html_element_root(&self) -> bool {
if self.element.local_name() != &local_name!("body") {
return false;
}
self.parent_element().is_some_and(|element| {
element.is_root() && element.element.local_name() == &local_name!("html")
})
}
/// Returns the parent element of this element, if it has one.
fn parent_element(&self) -> Option<Self> {
self.as_node().parent_element()
}
fn is_root(&self) -> bool {
match self.as_node().parent_node() {
None => false,
Some(node) => matches!(node.script_type_id(), NodeTypeId::Document(_)),
}
}
}
pub enum DOMDescendantIterator<E>
where
E: TElement,
{
/// Iterating over the children of a node, including children of a potential
/// [ShadowRoot](crate::dom::shadow_root::ShadowRoot)
Children(DomChildren<E::ConcreteNode>),
/// Iterating over the content's of a [`<slot>`](HTMLSlotElement) element.
Slottables { slot: E, index: usize },
}
impl<E> Iterator for DOMDescendantIterator<E>
where
E: TElement,
{
type Item = E::ConcreteNode;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Children(children) => children.next(),
Self::Slottables { slot, index } => {
let slottables = slot.slotted_nodes();
let slot = slottables.get(*index)?;
*index += 1;
Some(*slot)
},
}
}
}
impl<'dom> style::dom::AttributeProvider for ServoLayoutElement<'dom> {
fn get_attr(&self, attr: &style::LocalName) -> Option<String> {
self.element
.get_attr_val_for_layout(&ns!(), attr)
.map(String::from)
}
}
impl<'dom> style::dom::TElement for ServoLayoutElement<'dom> {
type ConcreteNode = ServoLayoutNode<'dom>;
type TraversalChildrenIterator = DOMDescendantIterator<Self>;
fn as_node(&self) -> ServoLayoutNode<'dom> {
ServoLayoutNode::from_layout_js(self.element.upcast())
}
fn traversal_children(&self) -> LayoutIterator<Self::TraversalChildrenIterator> {
let iterator = if self.slotted_nodes().is_empty() {
let children = if let Some(shadow_root) = self.shadow_root() {
shadow_root.as_node().dom_children()
} else {
self.as_node().dom_children()
};
DOMDescendantIterator::Children(children)
} else {
DOMDescendantIterator::Slottables {
slot: *self,
index: 0,
}
};
LayoutIterator(iterator)
}
fn traversal_parent(&self) -> Option<Self> {
self.as_node().traversal_parent()
}
fn inheritance_parent(&self) -> Option<Self> {
if self.is_pseudo_element() {
// The inheritance parent of an implemented pseudo-element should be the
// originating element, except if `is_element_backed()` is true, then it should
// be the flat tree parent. Note `is_element_backed()` differs from the CSS term.
// At the current time, `is_element_backed()` is always false in Servo.
//
// FIXME: handle the cases of element-backed pseudo-elements.
return self.pseudo_element_originating_element();
}
self.traversal_parent()
}
fn is_html_element(&self) -> bool {
ServoLayoutElement::is_html_element(self)
}
fn is_mathml_element(&self) -> bool {
*self.element.namespace() == ns!(mathml)
}
fn is_svg_element(&self) -> bool {
*self.element.namespace() == ns!(svg)
}
fn has_part_attr(&self) -> bool {
self.element
.get_attr_for_layout(&ns!(), &local_name!("part"))
.is_some()
}
fn exports_any_part(&self) -> bool {
self.element
.get_attr_for_layout(&ns!(), &local_name!("exportparts"))
.is_some()
}
fn style_attribute(&self) -> Option<ArcBorrow<'_, StyleLocked<PropertyDeclarationBlock>>> {
unsafe {
(*self.element.style_attribute())
.as_ref()
.map(|x| x.borrow_arc())
}
}
fn may_have_animations(&self) -> bool {
true
}
fn animation_rule(
&self,
context: &SharedStyleContext,
) -> Option<Arc<StyleLocked<PropertyDeclarationBlock>>> {
let node = self.as_node();
let document = node.owner_doc();
context.animations.get_animation_declarations(
&AnimationSetKey::new_for_non_pseudo(node.opaque()),
context.current_time_for_animations,
document.style_shared_lock(),
)
}
fn transition_rule(
&self,
context: &SharedStyleContext,
) -> Option<Arc<StyleLocked<PropertyDeclarationBlock>>> {
let node = self.as_node();
let document = node.owner_doc();
context.animations.get_transition_declarations(
&AnimationSetKey::new_for_non_pseudo(node.opaque()),
context.current_time_for_animations,
document.style_shared_lock(),
)
}
fn state(&self) -> ElementState {
self.element.get_state_for_layout()
}
#[inline]
fn id(&self) -> Option<&Atom> {
unsafe { (*self.element.id_attribute()).as_ref() }
}
#[inline(always)]
fn each_class<F>(&self, mut callback: F)
where
F: FnMut(&AtomIdent),
{
if let Some(classes) = self.element.get_classes_for_layout() {
for class in classes {
callback(AtomIdent::cast(class))
}
}
}
#[inline(always)]
fn each_attr_name<F>(&self, mut callback: F)
where
F: FnMut(&style::LocalName),
{
for attr in self.element.attrs() {
callback(style::values::GenericAtomIdent::cast(attr.local_name()))
}
}
fn each_part<F>(&self, mut callback: F)
where
F: FnMut(&AtomIdent),
{
if let Some(parts) = self.element.get_parts_for_layout() {
for part in parts {
callback(AtomIdent::cast(part))
}
}
}
fn each_exported_part<F>(&self, name: &AtomIdent, callback: F)
where
F: FnMut(&AtomIdent),
{
let Some(exported_parts) = self
.element
.get_attr_for_layout(&ns!(), &local_name!("exportparts"))
else {
return;
};
exported_parts
.as_shadow_parts()
.for_each_exported_part(AtomIdent::cast(name), callback);
}
fn has_dirty_descendants(&self) -> bool {
unsafe {
self.as_node()
.node
.get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS)
}
}
fn has_snapshot(&self) -> bool {
unsafe { self.as_node().node.get_flag(NodeFlags::HAS_SNAPSHOT) }
}
fn handled_snapshot(&self) -> bool {
unsafe { self.as_node().node.get_flag(NodeFlags::HANDLED_SNAPSHOT) }
}
unsafe fn set_handled_snapshot(&self) {
unsafe {
self.as_node()
.node
.set_flag(NodeFlags::HANDLED_SNAPSHOT, true);
}
}
unsafe fn set_dirty_descendants(&self) {
debug_assert!(self.as_node().is_connected());
unsafe {
self.as_node()
.node
.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, true)
}
}
unsafe fn unset_dirty_descendants(&self) {
unsafe {
self.as_node()
.node
.set_flag(NodeFlags::HAS_DIRTY_DESCENDANTS, false)
}
}
/// Whether this element should match user and content rules.
/// We would like to match rules from the same tree in all cases and optimize computation.
/// UA Widget is an exception since we could have a pseudo element selector inside it.
#[inline]
fn matches_user_and_content_rules(&self) -> bool {
!self.as_node().node.is_in_ua_widget()
}
/// Returns the pseudo-element implemented by this element, if any. In other words,
/// the element will match the specified pseudo element throughout the style computation.
#[inline]
fn implemented_pseudo_element(&self) -> Option<PseudoElement> {
self.as_node().node.implemented_pseudo_element()
}
fn store_children_to_process(&self, n: isize) {
let data = self.get_style_data().unwrap();
data.parallel
.children_to_process
.store(n, Ordering::Relaxed);
}
fn did_process_child(&self) -> isize {
let data = self.get_style_data().unwrap();
let old_value = data
.parallel
.children_to_process
.fetch_sub(1, Ordering::Relaxed);
debug_assert!(old_value >= 1);
old_value - 1
}
unsafe fn clear_data(&self) {
unsafe { self.as_node().get_jsmanaged().clear_style_and_layout_data() }
}
unsafe fn ensure_data(&self) -> AtomicRefMut<'_, ElementData> {
unsafe {
self.as_node().get_jsmanaged().initialize_style_data();
};
self.mutate_data().unwrap()
}
/// Whether there is an ElementData container.
fn has_data(&self) -> bool {
self.get_style_data().is_some()
}
/// Immutably borrows the ElementData.
fn borrow_data(&self) -> Option<AtomicRef<'_, ElementData>> {
self.get_style_data().map(|data| data.element_data.borrow())
}
/// Mutably borrows the ElementData.
fn mutate_data(&self) -> Option<AtomicRefMut<'_, ElementData>> {
self.get_style_data()
.map(|data| data.element_data.borrow_mut())
}
fn skip_item_display_fixup(&self) -> bool {
false
}
fn has_animations(&self, context: &SharedStyleContext) -> bool {
// This is not used for pseudo elements currently so we can pass None.
self.has_css_animations(context, /* pseudo_element = */ None) ||
self.has_css_transitions(context, /* pseudo_element = */ None)
}
fn has_css_animations(
&self,
context: &SharedStyleContext,
pseudo_element: Option<PseudoElement>,
) -> bool {
let key = AnimationSetKey::new(self.as_node().opaque(), pseudo_element);
context.animations.has_active_animations(&key)
}
fn has_css_transitions(
&self,
context: &SharedStyleContext,
pseudo_element: Option<PseudoElement>,
) -> bool {
let key = AnimationSetKey::new(self.as_node().opaque(), pseudo_element);
context.animations.has_active_transitions(&key)
}
#[inline]
fn lang_attr(&self) -> Option<SelectorAttrValue> {
self.get_attr(&ns!(xml), &local_name!("lang"))
.or_else(|| self.get_attr(&ns!(), &local_name!("lang")))
.map(|v| SelectorAttrValue::from(v as &str))
}
fn match_element_lang(
&self,
override_lang: Option<Option<SelectorAttrValue>>,
value: &Lang,
) -> bool {
// Servo supports :lang() from CSS Selectors 4, which can take a comma-
// separated list of language tags in the pseudo-class, and which
// performs RFC 4647 extended filtering matching on them.
//
// FIXME(heycam): This is wrong, since extended_filtering accepts
// a string containing commas (separating each language tag in
// a list) but the pseudo-class instead should be parsing and
// storing separate <ident> or <string>s for each language tag.
//
// FIXME(heycam): Look at `element`'s document's Content-Language
// HTTP header for language tags to match `value` against. To
// do this, we should make `get_lang_for_layout` return an Option,
// so we can decide when to fall back to the Content-Language check.
let element_lang = match override_lang {
Some(Some(lang)) => lang,
Some(None) => AtomString::default(),
None => AtomString::from(&*self.element.get_lang_for_layout()),
};
extended_filtering(&element_lang, value)
}
fn is_html_document_body_element(&self) -> bool {
self.is_body_element_of_html_element_root()
}
fn synthesize_presentational_hints_for_legacy_attributes<V>(
&self,
_visited_handling: VisitedHandlingMode,
hints: &mut V,
) where
V: Push<ApplicableDeclarationBlock>,
{
self.element
.synthesize_presentational_hints_for_legacy_attributes(hints);
}
/// The shadow root this element is a host of.
fn shadow_root(&self) -> Option<ServoShadowRoot<'dom>> {
self.element
.get_shadow_root_for_layout()
.map(ServoShadowRoot::from_layout_js)
}
/// The shadow root which roots the subtree this element is contained in.
fn containing_shadow(&self) -> Option<ServoShadowRoot<'dom>> {
self.element
.upcast()
.containing_shadow_root_for_layout()
.map(ServoShadowRoot::from_layout_js)
}
fn local_name(&self) -> &LocalName {
self.element.local_name()
}
fn namespace(&self) -> &Namespace {
self.element.namespace()
}
fn query_container_size(
&self,
_display: &Display,
) -> euclid::default::Size2D<Option<app_units::Au>> {
todo!();
}
fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool {
self.element.get_selector_flags().contains(flags)
}
fn relative_selector_search_direction(&self) -> ElementSelectorFlags {
self.element
.get_selector_flags()
.intersection(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING)
}
fn each_custom_state<F>(&self, callback: F)
where
F: FnMut(&AtomIdent),
{
self.element.each_custom_state_for_layout(callback);
}
/// Returns the implicit scope root for given sheet index and host.
fn implicit_scope_for_sheet_in_shadow_root(
opaque_host: ::selectors::OpaqueElement,
sheet_index: usize,
) -> Option<ImplicitScopeRoot> {
// As long as this "unopaqued" element does not escape this function, we're not leaking
// potentially-mutable elements from opaque elements.
let host = unsafe {
let ptr = opaque_host.as_const_ptr::<JSObject>();
let untrusted_address = UntrustedNodeAddress::from_id(ptr as usize);
let node = Node::from_untrusted_node_address(untrusted_address);
let trusted_address = node.to_trusted_node_address();
let servo_layout_node = ServoLayoutNode::new(&trusted_address);
servo_layout_node.as_element().unwrap()
};
host.shadow_root()?.implicit_scope_for_sheet(sheet_index)
}
fn slotted_nodes(&self) -> &[Self::ConcreteNode] {
let Some(slot_element) = self.element.unsafe_get().downcast::<HTMLSlotElement>() else {
return &[];
};
let assigned_nodes = slot_element.assigned_nodes();
// SAFETY:
// Self::ConcreteNode (aka ServoLayoutNode) and Slottable are guaranteed to have the same
// layout and alignment as ptr::NonNull<T>. Lifetimes are not an issue because the
// slottables are being kept alive by the slot element.
unsafe {
slice::from_raw_parts(
assigned_nodes.as_ptr() as *const Self::ConcreteNode,
assigned_nodes.len(),
)
}
}
fn compute_layout_damage(old: &ComputedValues, new: &ComputedValues) -> RestyleDamage {
let box_tree_needs_rebuild = || {
let old_box = old.get_box();
let new_box = new.get_box();
if old_box.display != new_box.display ||
old_box.float != new_box.float ||
old_box.position != new_box.position
{
return true;
}
if new_box.position.is_absolutely_positioned() &&
old_box.original_display != new_box.original_display
{
// The original display only affects the static position, which is only used
// when both insets in some axis are auto.
// <https://drafts.csswg.org/css-position/#resolving-insets>
let position = new.get_position();
if (position.top.is_auto() && position.bottom.is_auto()) ||
(position.left.is_auto() && position.right.is_auto())
{
return true;
}
}
if old.get_font() != new.get_font() {
return true;
}
if old.get_position().order != new.get_position().order {
return true;
}
// Only consider changes to the `quotes` attribute if they actually apply to this
// style (if it is a pseudo-element that supports it).
if matches!(
new.pseudo(),
Some(PseudoElement::Before | PseudoElement::After | PseudoElement::Marker),
) && old.get_list().quotes != new.get_list().quotes
{
return true;
}
// NOTE: This should be kept in sync with the checks in `impl
// StyleExt::establishes_block_formatting_context` for `ComputedValues` in
// `components/layout/style_ext.rs`.
if new_box.display.outside() == DisplayOutside::Block &&
new_box.display.inside() == DisplayInside::Flow
{
let alignment_establishes_new_block_formatting_context =
|style: &ComputedValues| {
style.get_position().align_content.primary() != AlignFlags::NORMAL
};
let old_column = old.get_column();
let new_column = new.get_column();
if old_box.overflow_x.is_scrollable() != new_box.overflow_x.is_scrollable() ||
old_column.is_multicol() != new_column.is_multicol() ||
old_column.column_span != new_column.column_span ||
alignment_establishes_new_block_formatting_context(old) !=
alignment_establishes_new_block_formatting_context(new)
{
return true;
}
}
if old_box.display.is_list_item() {
let old_list = old.get_list();
let new_list = new.get_list();
if old_list.list_style_position != new_list.list_style_position ||
old_list.list_style_image != new_list.list_style_image ||
(new_list.list_style_image == Image::None &&
old_list.list_style_type != new_list.list_style_type)
{
return true;
}
}
if new.is_pseudo_style() && old.get_counters().content != new.get_counters().content {
return true;
}
// If we're not a pseudo, `content` can still cause layout damage if its value is
// <content-replacement> (a.k.a. a single <image>).
fn replacement<Image>(content: &Content<Image>) -> Option<&Image> {
match content {
Content::Items(GenericContentItems { items, .. }) => match items.as_slice() {
[ContentItem::Image(image)] => Some(image),
_ => None,
},
_ => None,
}
}
if replacement(&old.get_counters().content) != replacement(&new.get_counters().content)
{
return true;
}
false
};
let text_shaping_needs_recollect = || {
if old.clone_direction() != new.clone_direction() ||
old.clone_unicode_bidi() != new.clone_unicode_bidi()
{
return true;
}
let old_text = old.get_inherited_text().clone();
let new_text = new.get_inherited_text().clone();
if old_text.white_space_collapse != new_text.white_space_collapse ||
old_text.text_transform != new_text.text_transform ||
old_text.word_break != new_text.word_break ||
old_text.overflow_wrap != new_text.overflow_wrap ||
old_text.letter_spacing != new_text.letter_spacing ||
old_text.word_spacing != new_text.word_spacing ||
old_text.text_rendering != new_text.text_rendering
{
return true;
}
false
};
if box_tree_needs_rebuild() {
RestyleDamage::from_bits_retain(LayoutDamage::BOX_DAMAGE.bits())
} else if text_shaping_needs_recollect() {
RestyleDamage::from_bits_retain(LayoutDamage::DESCENDANT_HAS_BOX_DAMAGE.bits())
} else {
// This element needs to be laid out again, but does not have any damage to
// its box. In the future, we will distinguish between types of damage to the
// fragment as well.
RestyleDamage::RELAYOUT
}
}
}
impl<'dom> ::selectors::Element for ServoLayoutElement<'dom> {
type Impl = SelectorImpl;
fn opaque(&self) -> ::selectors::OpaqueElement {
::selectors::OpaqueElement::new(unsafe { &*(self.as_node().opaque().0 as *const ()) })
}
fn parent_element(&self) -> Option<Self> {
ServoLayoutElement::parent_element(self)
}
fn parent_node_is_shadow_root(&self) -> bool {
match self.as_node().parent_node() {
None => false,
Some(node) => {
node.script_type_id() ==
NodeTypeId::DocumentFragment(DocumentFragmentTypeId::ShadowRoot)
},
}
}
fn containing_shadow_host(&self) -> Option<Self> {
self.containing_shadow().map(|s| s.host())
}
#[inline]
fn is_pseudo_element(&self) -> bool {
self.implemented_pseudo_element().is_some()
}
#[inline]
fn pseudo_element_originating_element(&self) -> Option<Self> {
debug_assert!(self.is_pseudo_element());
debug_assert!(!self.matches_user_and_content_rules());
self.containing_shadow_host()
}
fn prev_sibling_element(&self) -> Option<Self> {
let mut node = self.as_node();
while let Some(sibling) = node.prev_sibling() {
if let Some(element) = sibling.as_element() {
return Some(element);
}
node = sibling;
}
None
}
fn next_sibling_element(&self) -> Option<ServoLayoutElement<'dom>> {
let mut node = self.as_node();
while let Some(sibling) = node.next_sibling() {
if let Some(element) = sibling.as_element() {
return Some(element);
}
node = sibling;
}
None
}
fn first_element_child(&self) -> Option<Self> {
self.as_node()
.dom_children()
.find_map(|child| child.as_element())
}
fn attr_matches(
&self,
ns: &NamespaceConstraint<&style::Namespace>,
local_name: &style::LocalName,
operation: &AttrSelectorOperation<&AtomString>,
) -> bool {
match *ns {
NamespaceConstraint::Specific(ns) => self
.get_attr_enum(ns, local_name)
.is_some_and(|value| value.eval_selector(operation)),
NamespaceConstraint::Any => self
.element
.get_attr_vals_for_layout(local_name)
.any(|value| value.eval_selector(operation)),
}
}
fn is_root(&self) -> bool {
ServoLayoutElement::is_root(self)
}
fn is_empty(&self) -> bool {
self.as_node()
.dom_children()
.all(|node| match node.script_type_id() {
NodeTypeId::Element(..) => false,
NodeTypeId::CharacterData(CharacterDataTypeId::Text(TextTypeId::Text)) => {
node.node.downcast().unwrap().data_for_layout().is_empty()
},
_ => true,
})
}
#[inline]
fn has_local_name(&self, name: &LocalName) -> bool {
self.element.local_name() == name
}
#[inline]
fn has_namespace(&self, ns: &Namespace) -> bool {
self.element.namespace() == ns
}
#[inline]
fn is_same_type(&self, other: &Self) -> bool {
self.element.local_name() == other.element.local_name() &&
self.element.namespace() == other.element.namespace()
}
fn match_non_ts_pseudo_class(
&self,
pseudo_class: &NonTSPseudoClass,
_: &mut MatchingContext<Self::Impl>,
) -> bool {
match *pseudo_class {
// https://github.com/servo/servo/issues/8718
NonTSPseudoClass::Link | NonTSPseudoClass::AnyLink => self.is_link(),
NonTSPseudoClass::Visited => false,
NonTSPseudoClass::CustomState(ref state) => self.has_custom_state(&state.0),
NonTSPseudoClass::Lang(ref lang) => self.match_element_lang(None, lang),
NonTSPseudoClass::ServoNonZeroBorder => !matches!(
self.element
.get_attr_for_layout(&ns!(), &local_name!("border")),
None | Some(&AttrValue::UInt(_, 0))
),
NonTSPseudoClass::ReadOnly => !self
.element
.get_state_for_layout()
.contains(NonTSPseudoClass::ReadWrite.state_flag()),
NonTSPseudoClass::Active |
NonTSPseudoClass::Autofill |
NonTSPseudoClass::Checked |
NonTSPseudoClass::Default |
NonTSPseudoClass::Defined |
NonTSPseudoClass::Disabled |
NonTSPseudoClass::Enabled |
NonTSPseudoClass::Focus |
NonTSPseudoClass::FocusVisible |
NonTSPseudoClass::FocusWithin |
NonTSPseudoClass::Fullscreen |
NonTSPseudoClass::Hover |
NonTSPseudoClass::InRange |
NonTSPseudoClass::Indeterminate |
NonTSPseudoClass::Invalid |
NonTSPseudoClass::Modal |
NonTSPseudoClass::MozMeterOptimum |
NonTSPseudoClass::MozMeterSubOptimum |
NonTSPseudoClass::MozMeterSubSubOptimum |
NonTSPseudoClass::Open |
NonTSPseudoClass::Optional |
NonTSPseudoClass::OutOfRange |
NonTSPseudoClass::PlaceholderShown |
NonTSPseudoClass::PopoverOpen |
NonTSPseudoClass::ReadWrite |
NonTSPseudoClass::Required |
NonTSPseudoClass::Target |
NonTSPseudoClass::UserInvalid |
NonTSPseudoClass::UserValid |
NonTSPseudoClass::Valid => self
.element
.get_state_for_layout()
.contains(pseudo_class.state_flag()),
}
}
fn match_pseudo_element(
&self,
pseudo: &PseudoElement,
_context: &mut MatchingContext<Self::Impl>,
) -> bool {
self.implemented_pseudo_element() == Some(*pseudo)
}
#[inline]
fn is_link(&self) -> bool {
match self.as_node().script_type_id() {
// https://html.spec.whatwg.org/multipage/#selector-link
NodeTypeId::Element(ElementTypeId::HTMLElement(
HTMLElementTypeId::HTMLAnchorElement,
)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLAreaElement)) |
NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLLinkElement)) => {
self.element
.get_attr_val_for_layout(&ns!(), &local_name!("href"))
.is_some()
},
_ => false,
}
}
#[inline]
fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
unsafe {
(*self.element.id_attribute())
.as_ref()
.is_some_and(|atom| case_sensitivity.eq_atom(atom, id))
}
}
#[inline]
fn is_part(&self, name: &AtomIdent) -> bool {
self.element.has_class_or_part_for_layout(
name,
&local_name!("part"),
CaseSensitivity::CaseSensitive,
)
}
fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> {
self.element
.get_attr_for_layout(&ns!(), &local_name!("exportparts"))?
.as_shadow_parts()
.imported_part(name)
.map(|import| AtomIdent::new(import.clone()))
}
#[inline]
fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool {
self.element
.has_class_or_part_for_layout(name, &local_name!("class"), case_sensitivity)
}
fn is_html_slot_element(&self) -> bool {
self.element.is::<HTMLSlotElement>()
}
fn assigned_slot(&self) -> Option<Self> {
self.as_node().assigned_slot()
}
fn is_html_element_in_html_document(&self) -> bool {
self.element.is_html_element() && self.as_node().owner_doc().is_html_document()
}
fn apply_selector_flags(&self, flags: ElementSelectorFlags) {
// Handle flags that apply to the element.
let self_flags = flags.for_self();
if !self_flags.is_empty() {
self.element.insert_selector_flags(flags);
}
// Handle flags that apply to the parent.
let parent_flags = flags.for_parent();
if !parent_flags.is_empty() {
if let Some(p) = self.as_node().parent_element() {
p.element.insert_selector_flags(flags);
}
}
}
fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool {
each_relevant_element_hash(*self, |hash| filter.insert_hash(hash & BLOOM_HASH_MASK));
true
}
fn has_custom_state(&self, name: &AtomIdent) -> bool {
let mut has_state = false;
self.element
.each_custom_state_for_layout(|state| has_state |= state == name);
has_state
}
}
/// A wrapper around elements that ensures layout can only
/// ever access safe properties and cannot race on elements.
#[derive(Clone, Copy, Debug)]
pub struct ServoThreadSafeLayoutElement<'dom> {
/// The wrapped [`ServoLayoutElement`].
pub(super) element: ServoLayoutElement<'dom>,
/// The possibly nested [`PseudoElementChain`] for this element.
pub(super) pseudo_element_chain: PseudoElementChain,
}
impl<'dom> ServoThreadSafeLayoutElement<'dom> {
/// The shadow root this element is a host of.
pub fn shadow_root(&self) -> Option<ServoShadowRoot<'dom>> {
self.element
.element
.get_shadow_root_for_layout()
.map(ServoShadowRoot::from_layout_js)
}
pub fn slotted_nodes(&self) -> &[ServoLayoutNode<'dom>] {
self.element.slotted_nodes()
}
}
impl<'dom> ThreadSafeLayoutElement<'dom> for ServoThreadSafeLayoutElement<'dom> {
type ConcreteThreadSafeLayoutNode = ServoThreadSafeLayoutNode<'dom>;
type ConcreteElement = ServoLayoutElement<'dom>;
fn as_node(&self) -> ServoThreadSafeLayoutNode<'dom> {
ServoThreadSafeLayoutNode {
node: self.element.as_node(),
pseudo_element_chain: self.pseudo_element_chain,
}
}
fn pseudo_element_chain(&self) -> PseudoElementChain {
self.pseudo_element_chain
}
fn with_pseudo(&self, pseudo_element: PseudoElement) -> Option<Self> {
if pseudo_element.is_eager() &&
self.style_data()
.styles
.pseudos
.get(&pseudo_element)
.is_none()
{
return None;
}
if pseudo_element == PseudoElement::DetailsSummary &&
(!self.has_local_name(&local_name!("details")) || !self.has_namespace(&ns!(html)))
{
return None;
}
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())
{
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_element_chain: self.pseudo_element_chain.with_pseudo(pseudo_element),
})
}
fn type_id(&self) -> Option<LayoutNodeType> {
self.as_node().type_id()
}
fn unsafe_get(self) -> ServoLayoutElement<'dom> {
self.element
}
fn get_local_name(&self) -> &LocalName {
self.element.local_name()
}
fn get_attr_enum(&self, namespace: &Namespace, name: &LocalName) -> Option<&AttrValue> {
self.element.get_attr_enum(namespace, name)
}
fn get_attr<'a>(&'a self, namespace: &Namespace, name: &LocalName) -> Option<&'a str> {
self.element.get_attr(namespace, name)
}
fn style_data(&self) -> AtomicRef<'_, ElementData> {
self.element.borrow_data().expect("Unstyled layout node?")
}
fn is_shadow_host(&self) -> bool {
self.element.shadow_root().is_some()
}
fn is_body_element_of_html_element_root(&self) -> bool {
self.element.is_html_document_body_element()
}
fn is_root(&self) -> bool {
self.element.is_root()
}
}
/// This implementation of `::selectors::Element` is used for implementing lazy
/// pseudo-elements.
///
/// Lazy pseudo-elements in Servo only allows selectors using safe properties,
/// i.e., local_name, attributes, so they can only be used for **private**
/// pseudo-elements (like `::details-content`).
///
/// Probably a few more of this functions can be implemented (like `has_class`, etc.),
/// but they have no use right now.
///
/// Note that the element implementation is needed only for selector matching,
/// not for inheritance (styles are inherited appropriately).
impl ::selectors::Element for ServoThreadSafeLayoutElement<'_> {
type Impl = SelectorImpl;
fn opaque(&self) -> ::selectors::OpaqueElement {
::selectors::OpaqueElement::new(unsafe { &*(self.as_node().opaque().0 as *const ()) })
}
fn parent_element(&self) -> Option<Self> {
warn!("ServoThreadSafeLayoutElement::parent_element called");
None
}
#[inline]
fn parent_node_is_shadow_root(&self) -> bool {
self.element.parent_node_is_shadow_root()
}
#[inline]
fn containing_shadow_host(&self) -> Option<Self> {
self.element
.containing_shadow_host()
.and_then(|element| element.as_node().to_threadsafe().as_element())
}
#[inline]
fn is_pseudo_element(&self) -> bool {
self.element.is_pseudo_element()
}
#[inline]
fn pseudo_element_originating_element(&self) -> Option<Self> {
self.element
.pseudo_element_originating_element()
.and_then(|element| element.as_node().to_threadsafe().as_element())
}
// Skips non-element nodes
fn prev_sibling_element(&self) -> Option<Self> {
warn!("ServoThreadSafeLayoutElement::prev_sibling_element called");
None
}
// Skips non-element nodes
fn next_sibling_element(&self) -> Option<Self> {
warn!("ServoThreadSafeLayoutElement::next_sibling_element called");
None
}
// Skips non-element nodes
fn first_element_child(&self) -> Option<Self> {
warn!("ServoThreadSafeLayoutElement::first_element_child called");
None
}
fn is_html_slot_element(&self) -> bool {
self.element.is_html_slot_element()
}
fn is_html_element_in_html_document(&self) -> bool {
self.element.is_html_element_in_html_document()
}
#[inline]
fn has_local_name(&self, name: &LocalName) -> bool {
self.element.local_name() == name
}
#[inline]
fn has_namespace(&self, ns: &Namespace) -> bool {
self.element.namespace() == ns
}
#[inline]
fn is_same_type(&self, other: &Self) -> bool {
self.element.local_name() == other.element.local_name() &&
self.element.namespace() == other.element.namespace()
}
fn attr_matches(
&self,
ns: &NamespaceConstraint<&style::Namespace>,
local_name: &style::LocalName,
operation: &AttrSelectorOperation<&AtomString>,
) -> bool {
match *ns {
NamespaceConstraint::Specific(ns) => self
.get_attr_enum(ns, local_name)
.is_some_and(|value| value.eval_selector(operation)),
NamespaceConstraint::Any => self
.element
.element
.get_attr_vals_for_layout(local_name)
.any(|v| v.eval_selector(operation)),
}
}
fn match_non_ts_pseudo_class(
&self,
_: &NonTSPseudoClass,
_: &mut MatchingContext<Self::Impl>,
) -> bool {
// NB: This could maybe be implemented
warn!("ServoThreadSafeLayoutElement::match_non_ts_pseudo_class called");
false
}
fn match_pseudo_element(
&self,
pseudo: &PseudoElement,
context: &mut MatchingContext<Self::Impl>,
) -> bool {
self.element.match_pseudo_element(pseudo, context)
}
fn is_link(&self) -> bool {
warn!("ServoThreadSafeLayoutElement::is_link called");
false
}
fn has_id(&self, _id: &AtomIdent, _case_sensitivity: CaseSensitivity) -> bool {
debug!("ServoThreadSafeLayoutElement::has_id called");
false
}
#[inline]
fn is_part(&self, _name: &AtomIdent) -> bool {
debug!("ServoThreadSafeLayoutElement::is_part called");
false
}
fn imported_part(&self, _: &AtomIdent) -> Option<AtomIdent> {
debug!("ServoThreadSafeLayoutElement::imported_part called");
None
}
fn has_class(&self, _name: &AtomIdent, _case_sensitivity: CaseSensitivity) -> bool {
debug!("ServoThreadSafeLayoutElement::has_class called");
false
}
fn is_empty(&self) -> bool {
warn!("ServoThreadSafeLayoutElement::is_empty called");
false
}
fn is_root(&self) -> bool {
warn!("ServoThreadSafeLayoutElement::is_root called");
false
}
fn apply_selector_flags(&self, flags: ElementSelectorFlags) {
// Handle flags that apply to the element.
let self_flags = flags.for_self();
if !self_flags.is_empty() {
self.element.element.insert_selector_flags(flags);
}
// Handle flags that apply to the parent.
let parent_flags = flags.for_parent();
if !parent_flags.is_empty() {
if let Some(p) = self.element.parent_element() {
p.element.insert_selector_flags(flags);
}
}
}
fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool {
each_relevant_element_hash(self.element, |hash| {
filter.insert_hash(hash & BLOOM_HASH_MASK)
});
true
}
fn has_custom_state(&self, _name: &AtomIdent) -> bool {
false
}
}