mirror of
https://github.com/servo/servo
synced 2026-04-26 01:25:32 +02:00
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>
563 lines
19 KiB
Rust
563 lines
19 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/. */
|
||
|
||
#![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 node’s 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<'_> {}
|