mirror of
https://github.com/servo/servo
synced 2026-05-09 00:22:16 +02:00
This change makes it so that all form controls are implemented with shadow DOM, completely removing the legacy text content and selection code paths for form controls. The motivation for this change is: - to allow properly hit testing against the text nodes of `<textarea>` and other widgets. This is important for implementing mouse-based selection on the page. - to simplify the way that form controls are implemented in general and to prepare the way for proper implementations of the user interface of other controls. Testing: This should not change observable behavior at the moment,so should be covered by existing WPT tests. --------- Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Oriol Brufau <obrufau@igalia.com>
532 lines
18 KiB
Rust
532 lines
18 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 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 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 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 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> {
|
|
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<'_> {}
|