| /* 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/. */ |
| |
| #![allow(unsafe_code)] |
| |
| use std::borrow::Cow; |
| use std::fmt; |
| use std::iter::FusedIterator; |
| |
| use base::id::{BrowsingContextId, PipelineId}; |
| use fonts_traits::ByteIndex; |
| use layout_api::wrapper_traits::{ |
| LayoutDataTrait, LayoutNode, PseudoElementChain, ThreadSafeLayoutElement, ThreadSafeLayoutNode, |
| }; |
| use layout_api::{ |
| GenericLayoutData, HTMLCanvasData, HTMLMediaData, LayoutElementType, LayoutNodeType, |
| SVGElementData, StyleData, TrustedNodeAddress, |
| }; |
| use net_traits::image_cache::Image; |
| use pixels::ImageMetadata; |
| use range::Range; |
| use selectors::Element as _; |
| use servo_arc::Arc; |
| use servo_url::ServoUrl; |
| use style; |
| use style::dom::{NodeInfo, TElement, TNode, TShadowRoot}; |
| use style::properties::ComputedValues; |
| use style::selector_parser::PseudoElement; |
| |
| use super::{ |
| ServoLayoutDocument, ServoLayoutElement, ServoShadowRoot, ServoThreadSafeLayoutElement, |
| }; |
| use crate::dom::bindings::inheritance::NodeTypeId; |
| use crate::dom::bindings::root::LayoutDom; |
| use crate::dom::element::{Element, LayoutElementHelpers}; |
| use crate::dom::node::{LayoutNodeHelpers, 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(el) = self.as_element() { |
| el.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(super) fn from_layout_js(n: LayoutDom<'dom, Node>) -> Self { |
| ServoLayoutNode { node: n } |
| } |
| |
| /// 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_js(node) |
| } |
| |
| pub(super) fn script_type_id(&self) -> NodeTypeId { |
| self.node.type_id_for_layout() |
| } |
| |
| /// Returns the interior of this node as a `LayoutDom`. |
| pub(crate) fn get_jsmanaged(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_js) |
| } |
| } |
| |
| 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_js) |
| } |
| |
| fn first_child(&self) -> Option<Self> { |
| self.node.first_child_ref().map(Self::from_layout_js) |
| } |
| |
| fn last_child(&self) -> Option<Self> { |
| self.node.last_child_ref().map(Self::from_layout_js) |
| } |
| |
| fn prev_sibling(&self) -> Option<Self> { |
| self.node.prev_sibling_ref().map(Self::from_layout_js) |
| } |
| |
| fn next_sibling(&self) -> Option<Self> { |
| self.node.next_sibling_ref().map(Self::from_layout_js) |
| } |
| |
| fn owner_doc(&self) -> Self::ConcreteDocument { |
| ServoLayoutDocument::from_layout_js(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.get_jsmanaged().opaque() |
| } |
| |
| fn debug_id(self) -> usize { |
| self.opaque().0 |
| } |
| |
| fn as_element(&self) -> Option<ServoLayoutElement<'dom>> { |
| self.node.downcast().map(ServoLayoutElement::from_layout_js) |
| } |
| |
| fn as_document(&self) -> Option<ServoLayoutDocument<'dom>> { |
| self.node |
| .downcast() |
| .map(ServoLayoutDocument::from_layout_js) |
| } |
| |
| fn as_shadow_root(&self) -> Option<ServoShadowRoot<'dom>> { |
| self.node.downcast().map(ServoShadowRoot::from_layout_js) |
| } |
| |
| 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>; |
| |
| fn to_threadsafe(&self) -> Self::ConcreteThreadSafeLayoutNode { |
| ServoThreadSafeLayoutNode::new(*self) |
| } |
| |
| fn type_id(&self) -> LayoutNodeType { |
| NodeTypeIdWrapper(self.script_type_id()).into() |
| } |
| |
| unsafe fn initialize_style_and_layout_data<RequestedLayoutDataType: LayoutDataTrait>(&self) { |
| let inner = self.get_jsmanaged(); |
| if inner.style_data().is_none() { |
| unsafe { inner.initialize_style_data() }; |
| } |
| if inner.layout_data().is_none() { |
| unsafe { inner.initialize_layout_data(Box::<RequestedLayoutDataType>::default()) }; |
| } |
| } |
| |
| fn is_connected(&self) -> bool { |
| unsafe { self.node.get_flag(NodeFlags::IS_CONNECTED) } |
| } |
| |
| fn style_data(&self) -> Option<&'dom StyleData> { |
| self.get_jsmanaged().style_data() |
| } |
| |
| fn layout_data(&self) -> Option<&'dom GenericLayoutData> { |
| self.get_jsmanaged().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.get_jsmanaged() |
| } |
| |
| /// 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_js) |
| .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_js) |
| .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 is_text_input(&self) -> bool { |
| self.node.node.is_text_input() |
| } |
| |
| pub fn selected_style(&self) -> 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(); |
| }; |
| |
| let style_data = &element.style_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()); |
| } |
| 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) -> Arc<ComputedValues> { |
| 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.get_jsmanaged(); |
| 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 |
| .as_element() |
| .map(|el| ServoThreadSafeLayoutElement { |
| element: el, |
| 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 style_data(&self) -> Option<&'dom StyleData> { |
| self.node.style_data() |
| } |
| |
| fn layout_data(&self) -> Option<&'dom GenericLayoutData> { |
| self.node.layout_data() |
| } |
| |
| fn unsafe_get(self) -> Self::ConcreteNode { |
| self.node |
| } |
| |
| fn node_text_content(self) -> Cow<'dom, str> { |
| unsafe { self.get_jsmanaged().text_content() } |
| } |
| |
| fn selection(&self) -> Option<Range<ByteIndex>> { |
| let this = unsafe { self.get_jsmanaged() }; |
| |
| this.selection().map(|range| { |
| Range::new( |
| ByteIndex(range.start as isize), |
| ByteIndex(range.len() as isize), |
| ) |
| }) |
| } |
| |
| 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 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> { |
| 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, |
| } |
| } |
| } |
| |
| 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> { |
| #[allow(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() { |
| #[allow(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<'_> {} |