Files
servo/components/script/layout_dom/node.rs
Martin Robinson 12a655d42c script: Move StyleData to Element from Node (#44074)
Many things are nodes, such as `CharacterData` and attributes. They
don't need style data at all. This saves 16 bytes on every attribute and
text node.

Testing: This should not change testable behavior (only save some
memory),
so it should be covered by existing tests.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
2026-04-11 07:46:28 +00:00

563 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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/. */
#![expect(unsafe_code)]
use std::borrow::Cow;
use std::fmt;
use std::iter::FusedIterator;
use layout_api::wrapper_traits::{
LayoutDataTrait, LayoutNode, PseudoElementChain, SharedSelection, ThreadSafeLayoutElement,
ThreadSafeLayoutNode,
};
use layout_api::{
GenericLayoutData, HTMLCanvasData, HTMLMediaData, LayoutElementType, LayoutNodeType,
SVGElementData, TrustedNodeAddress,
};
use net_traits::image_cache::Image;
use pixels::ImageMetadata;
use script_bindings::error::Fallible;
use selectors::Element as _;
use servo_arc::Arc;
use servo_base::id::{BrowsingContextId, PipelineId};
use servo_url::ServoUrl;
use style;
use style::context::SharedStyleContext;
use style::dom::{NodeInfo, TElement, TNode, TShadowRoot};
use style::dom_apis::{MayUseInvalidation, SelectorQuery, query_selector};
use style::properties::ComputedValues;
use style::selector_parser::{PseudoElement, SelectorParser};
use style::stylesheets::UrlExtraData;
use url::Url;
use super::{
ServoLayoutDocument, ServoLayoutElement, ServoShadowRoot, ServoThreadSafeLayoutElement,
};
use crate::dom::bindings::error::Error;
use crate::dom::bindings::inheritance::NodeTypeId;
use crate::dom::bindings::root::LayoutDom;
use crate::dom::element::Element;
use crate::dom::node::{Node, NodeFlags, NodeTypeIdWrapper};
/// A wrapper around a `LayoutDom<Node>` which provides a safe interface that
/// can be used during layout. This implements the `LayoutNode` trait as well as
/// several style and selectors traits for use during layout. This version
/// should only be used on a single thread. If you need to use nodes across
/// threads use ServoThreadSafeLayoutNode.
#[derive(Clone, Copy, PartialEq)]
#[repr(transparent)]
pub struct ServoLayoutNode<'dom> {
/// The wrapped private DOM node.
pub(super) node: LayoutDom<'dom, Node>,
}
/// Those are supposed to be sound, but they aren't because the entire system
/// between script and layout so far has been designed to work around their
/// absence. Switching the entire thing to the inert crate infra will help.
///
/// FIXME(mrobinson): These are required because Layout 2020 sends non-threadsafe
/// nodes to different threads. This should be adressed in a comprehensive way.
unsafe impl Send for ServoLayoutNode<'_> {}
unsafe impl Sync for ServoLayoutNode<'_> {}
impl fmt::Debug for ServoLayoutNode<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(element) = self.as_element() {
element.fmt(f)
} else if self.is_text_node() {
write!(f, "<text node> ({:#x})", self.opaque().0)
} else {
write!(f, "<non-text node> ({:#x})", self.opaque().0)
}
}
}
impl<'dom> ServoLayoutNode<'dom> {
pub(crate) fn from_layout_dom(node: LayoutDom<'dom, Node>) -> Self {
ServoLayoutNode { node }
}
/// Create a new [`ServoLayoutNode`] for this given [`TrustedNodeAddress`].
///
/// # Safety
///
/// The address pointed to by `address` should point to a valid node in memory.
pub unsafe fn new(address: &TrustedNodeAddress) -> Self {
let node = unsafe { LayoutDom::from_trusted_node_address(*address) };
ServoLayoutNode::from_layout_dom(node)
}
pub(super) fn script_type_id(&self) -> NodeTypeId {
self.node.type_id_for_layout()
}
/// Returns the interior of this node as a `LayoutDom`.
///
/// This method must never be exposed to layout as it returns
/// a `LayoutDom`.
pub(crate) fn to_layout_dom(self) -> LayoutDom<'dom, Node> {
self.node
}
pub(crate) fn assigned_slot(self) -> Option<ServoLayoutElement<'dom>> {
self.node
.assigned_slot_for_layout()
.as_ref()
.map(LayoutDom::upcast)
.map(ServoLayoutElement::from_layout_dom)
}
/// <https://dom.spec.whatwg.org/#scope-match-a-selectors-string>
pub(crate) fn scope_match_a_selectors_string<Query>(
self,
document_url: Arc<Url>,
selector: &str,
) -> Fallible<Query::Output>
where
Query: SelectorQuery<ServoLayoutElement<'dom>>,
Query::Output: Default,
{
let mut result = Query::Output::default();
// Step 1. Let selector be the result of parse a selector selectors.
let selector_or_error =
SelectorParser::parse_author_origin_no_namespace(selector, &UrlExtraData(document_url));
// Step 2. If selector is failure, then throw a "SyntaxError" DOMException.
let Ok(selector_list) = selector_or_error else {
return Err(Error::Syntax(None));
};
// Step 3. Return the result of match a selector against a tree with selector
// and nodes root using scoping root node.
query_selector::<ServoLayoutElement<'dom>, Query>(
self,
&selector_list,
&mut result,
MayUseInvalidation::No,
);
Ok(result)
}
}
impl style::dom::NodeInfo for ServoLayoutNode<'_> {
fn is_element(&self) -> bool {
self.node.is_element_for_layout()
}
fn is_text_node(&self) -> bool {
self.node.is_text_node_for_layout()
}
}
impl<'dom> style::dom::TNode for ServoLayoutNode<'dom> {
type ConcreteDocument = ServoLayoutDocument<'dom>;
type ConcreteElement = ServoLayoutElement<'dom>;
type ConcreteShadowRoot = ServoShadowRoot<'dom>;
fn parent_node(&self) -> Option<Self> {
self.node.parent_node_ref().map(Self::from_layout_dom)
}
fn first_child(&self) -> Option<Self> {
self.node.first_child_ref().map(Self::from_layout_dom)
}
fn last_child(&self) -> Option<Self> {
self.node.last_child_ref().map(Self::from_layout_dom)
}
fn prev_sibling(&self) -> Option<Self> {
self.node.prev_sibling_ref().map(Self::from_layout_dom)
}
fn next_sibling(&self) -> Option<Self> {
self.node.next_sibling_ref().map(Self::from_layout_dom)
}
fn owner_doc(&self) -> Self::ConcreteDocument {
ServoLayoutDocument::from_layout_dom(self.node.owner_doc_for_layout())
}
fn traversal_parent(&self) -> Option<ServoLayoutElement<'dom>> {
if let Some(assigned_slot) = self.assigned_slot() {
return Some(assigned_slot);
}
let parent = self.parent_node()?;
if let Some(shadow) = parent.as_shadow_root() {
return Some(shadow.host());
};
parent.as_element()
}
fn opaque(&self) -> style::dom::OpaqueNode {
self.to_layout_dom().opaque()
}
fn debug_id(self) -> usize {
self.opaque().0
}
fn as_element(&self) -> Option<ServoLayoutElement<'dom>> {
self.node
.downcast()
.map(ServoLayoutElement::from_layout_dom)
}
fn as_document(&self) -> Option<ServoLayoutDocument<'dom>> {
self.node
.downcast()
.map(ServoLayoutDocument::from_layout_dom)
}
fn as_shadow_root(&self) -> Option<ServoShadowRoot<'dom>> {
self.node.downcast().map(ServoShadowRoot::from_layout_dom)
}
fn is_in_document(&self) -> bool {
unsafe { self.node.get_flag(NodeFlags::IS_IN_A_DOCUMENT_TREE) }
}
}
impl<'dom> LayoutNode<'dom> for ServoLayoutNode<'dom> {
type ConcreteThreadSafeLayoutNode = ServoThreadSafeLayoutNode<'dom>;
type ConcreteLayoutElement = ServoLayoutElement<'dom>;
fn to_threadsafe(&self) -> Self::ConcreteThreadSafeLayoutNode {
ServoThreadSafeLayoutNode::new(*self)
}
fn type_id(&self) -> LayoutNodeType {
NodeTypeIdWrapper(self.script_type_id()).into()
}
fn is_connected(&self) -> bool {
unsafe { self.node.get_flag(NodeFlags::IS_CONNECTED) }
}
fn layout_data(&self) -> Option<&'dom GenericLayoutData> {
self.to_layout_dom().layout_data()
}
}
/// A wrapper around a `ServoLayoutNode` that can be used safely on different threads.
/// It's very important that this never mutate anything except this wrapped node and
/// never access any other node apart from its parent.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ServoThreadSafeLayoutNode<'dom> {
/// The wrapped [`ServoLayoutNode`].
pub(super) node: ServoLayoutNode<'dom>,
/// 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_element_chain: Default::default(),
}
}
/// Returns the interior of this node as a `LayoutDom`. This is highly unsafe for layout to
/// call and as such is marked `unsafe`.
unsafe fn get_jsmanaged(&self) -> LayoutDom<'dom, Node> {
self.node.to_layout_dom()
}
/// Get the first child of this node. Important: this is not safe for
/// layout to call, so it should *never* be made public.
unsafe fn dangerous_first_child(&self) -> Option<Self> {
let js_managed = unsafe { self.get_jsmanaged() };
js_managed
.first_child_ref()
.map(ServoLayoutNode::from_layout_dom)
.map(Self::new)
}
/// Get the next sibling of this node. Important: this is not safe for
/// layout to call, so it should *never* be made public.
unsafe fn dangerous_next_sibling(&self) -> Option<Self> {
let js_managed = unsafe { self.get_jsmanaged() };
js_managed
.next_sibling_ref()
.map(ServoLayoutNode::from_layout_dom)
.map(Self::new)
}
/// Whether this is a container for the text within a single-line text input. This
/// is used to solve the special case of line height for a text entry widget.
/// <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget>
// TODO(stevennovaryo): Remove the addition of HTMLInputElement here once all of the
// input element is implemented with UA shadow DOM. This is temporary
// workaround for past version of input element where we are
// 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_element_chain.is_empty() &&
self.node.node.is_text_container_of_single_line_input())
}
pub fn selected_style(&self, context: &SharedStyleContext) -> Arc<ComputedValues> {
let Some(element) = self.as_element() else {
// TODO(stshine): What should the selected style be for text?
debug_assert!(self.is_text_node());
return self.parent_style(context);
};
let style_data = &element.element_data().styles;
let get_selected_style = || {
// This is a workaround for handling the `::selection` pseudos where it would not
// propagate to the children and Shadow DOM elements. For this case, UA widget
// inner elements should follow the originating element in terms of selection.
if self.node.node.is_in_ua_widget() {
return Some(
element
.containing_shadow_host()?
.as_node()
.selected_style(context),
);
}
style_data.pseudos.get(&PseudoElement::Selection).cloned()
};
get_selected_style().unwrap_or_else(|| style_data.primary().clone())
}
}
impl style::dom::NodeInfo for ServoThreadSafeLayoutNode<'_> {
fn is_element(&self) -> bool {
self.node.is_element()
}
fn is_text_node(&self) -> bool {
self.node.is_text_node()
}
}
impl<'dom> ThreadSafeLayoutNode<'dom> for ServoThreadSafeLayoutNode<'dom> {
type ConcreteNode = ServoLayoutNode<'dom>;
type ConcreteThreadSafeLayoutElement = ServoThreadSafeLayoutElement<'dom>;
type ConcreteElement = ServoLayoutElement<'dom>;
type ChildrenIterator = ServoThreadSafeLayoutNodeChildrenIterator<'dom>;
fn opaque(&self) -> style::dom::OpaqueNode {
unsafe { self.get_jsmanaged().opaque() }
}
fn pseudo_element_chain(&self) -> PseudoElementChain {
self.pseudo_element_chain
}
fn type_id(&self) -> Option<LayoutNodeType> {
if self.pseudo_element_chain.is_empty() {
Some(self.node.type_id())
} else {
None
}
}
fn parent_style(&self, context: &SharedStyleContext) -> Arc<ComputedValues> {
if let Some(chain) = self.pseudo_element_chain.without_innermost() {
let mut parent = *self;
parent.pseudo_element_chain = chain;
return parent.style(context);
}
let parent_element = self.node.traversal_parent().unwrap();
let parent_data = parent_element.borrow_data().unwrap();
parent_data.styles.primary().clone()
}
fn initialize_layout_data<RequestedLayoutDataType: LayoutDataTrait>(&self) {
let inner = self.node.to_layout_dom();
if inner.layout_data().is_none() {
unsafe {
inner.initialize_layout_data(Box::<RequestedLayoutDataType>::default());
}
}
}
fn debug_id(self) -> usize {
self.node.debug_id()
}
fn children(&self) -> style::dom::LayoutIterator<Self::ChildrenIterator> {
style::dom::LayoutIterator(ServoThreadSafeLayoutNodeChildrenIterator::new(*self))
}
fn as_element(&self) -> Option<ServoThreadSafeLayoutElement<'dom>> {
self.node
.node
.downcast()
.map(|element| ServoThreadSafeLayoutElement {
element: ServoLayoutElement::from_layout_dom(element),
pseudo_element_chain: self.pseudo_element_chain,
})
}
fn as_html_element(&self) -> Option<ServoThreadSafeLayoutElement<'dom>> {
self.as_element()
.filter(|element| element.element.is_html_element())
}
fn layout_data(&self) -> Option<&'dom GenericLayoutData> {
self.node.layout_data()
}
fn unsafe_get(self) -> Self::ConcreteNode {
self.node
}
fn text_content(self) -> Cow<'dom, str> {
unsafe { self.get_jsmanaged().text_content() }
}
fn selection(&self) -> Option<SharedSelection> {
let this = unsafe { self.get_jsmanaged() };
this.selection()
}
fn image_url(&self) -> Option<ServoUrl> {
let this = unsafe { self.get_jsmanaged() };
this.image_url()
}
fn image_density(&self) -> Option<f64> {
let this = unsafe { self.get_jsmanaged() };
this.image_density()
}
fn showing_broken_image_icon(&self) -> bool {
let this = unsafe { self.get_jsmanaged() };
this.showing_broken_image_icon()
}
fn image_data(&self) -> Option<(Option<Image>, Option<ImageMetadata>)> {
let this = unsafe { self.get_jsmanaged() };
this.image_data()
}
fn canvas_data(&self) -> Option<HTMLCanvasData> {
let this = unsafe { self.get_jsmanaged() };
this.canvas_data()
}
fn media_data(&self) -> Option<HTMLMediaData> {
let this = unsafe { self.get_jsmanaged() };
this.media_data()
}
fn svg_data(&self) -> Option<SVGElementData<'dom>> {
let this = unsafe { self.get_jsmanaged() };
this.svg_data()
}
// Can return None if the iframe has no nested browsing context
fn iframe_browsing_context_id(&self) -> Option<BrowsingContextId> {
let this = unsafe { self.get_jsmanaged() };
this.iframe_browsing_context_id()
}
// Can return None if the iframe has no nested browsing context
fn iframe_pipeline_id(&self) -> Option<PipelineId> {
let this = unsafe { self.get_jsmanaged() };
this.iframe_pipeline_id()
}
fn get_span(&self) -> Option<u32> {
unsafe {
self.get_jsmanaged()
.downcast::<Element>()
.unwrap()
.get_span()
}
}
fn get_colspan(&self) -> Option<u32> {
unsafe {
self.get_jsmanaged()
.downcast::<Element>()
.unwrap()
.get_colspan()
}
}
fn get_rowspan(&self) -> Option<u32> {
unsafe {
self.get_jsmanaged()
.downcast::<Element>()
.unwrap()
.get_rowspan()
}
}
fn with_pseudo_element_chain(&self, pseudo_element_chain: PseudoElementChain) -> Self {
Self {
node: self.node,
pseudo_element_chain,
}
}
/// # Safety
///
/// This function accesses and modifies the underlying DOM object and should
/// not be used by more than a single thread at once.
fn set_uses_content_attribute_with_attr(&self, uses_content_attribute_with_attr: bool) {
unsafe {
self.node.node.set_flag(
NodeFlags::USES_ATTR_IN_CONTENT_ATTRIBUTE,
uses_content_attribute_with_attr,
)
}
}
}
pub enum ServoThreadSafeLayoutNodeChildrenIterator<'dom> {
/// Iterating over the children of a node
Node(Option<ServoThreadSafeLayoutNode<'dom>>),
/// Iterating over the assigned nodes of a `HTMLSlotElement`
Slottables(<Vec<ServoLayoutNode<'dom>> as IntoIterator>::IntoIter),
}
impl<'dom> ServoThreadSafeLayoutNodeChildrenIterator<'dom> {
#[expect(unsafe_code)]
fn new(
parent: ServoThreadSafeLayoutNode<'dom>,
) -> ServoThreadSafeLayoutNodeChildrenIterator<'dom> {
if let Some(element) = parent.as_element() {
if let Some(shadow) = element.shadow_root() {
return Self::new(shadow.as_node().to_threadsafe());
};
let slotted_nodes = element.slotted_nodes();
if !slotted_nodes.is_empty() {
#[expect(clippy::unnecessary_to_owned)] // Clippy is wrong.
return Self::Slottables(slotted_nodes.to_owned().into_iter());
}
}
Self::Node(unsafe { parent.dangerous_first_child() })
}
}
impl<'dom> Iterator for ServoThreadSafeLayoutNodeChildrenIterator<'dom> {
type Item = ServoThreadSafeLayoutNode<'dom>;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Node(node) => {
let next_sibling = unsafe { (*node)?.dangerous_next_sibling() };
std::mem::replace(node, next_sibling)
},
Self::Slottables(slots) => slots.next().map(|node| node.to_threadsafe()),
}
}
}
impl FusedIterator for ServoThreadSafeLayoutNodeChildrenIterator<'_> {}