From 378c4648e465d793aa41f7a7a74da59462d3a487 Mon Sep 17 00:00:00 2001 From: Steven Novaryo <65610990+stevennovaryo@users.noreply.github.com> Date: Wed, 9 Jul 2025 23:36:58 +0800 Subject: [PATCH] script: Use an implemented pseudo-element to for`type=color` `::color-swatch` (#37427) Implement internal pseudo element, which would be resolved as a "Implemented Pseudo Element" within style computation. This is an concrete element that would has a primary style after the style computation, but could match and style resolved like an pseudo element. Therefore, it would have a different behavior compared to how does `pseudo`s that `ServoLayoutNode` had. Where they would not have a concrete element behind it. Note that, due to the nature of these pseudo elements residing inside a UA widget, these pseudo elements would therefore not be accessible in JavaScript by default. This kind of element is required in order to implement the [form control pseudo element](https://drafts.csswg.org/css-forms-1/#pseudo-elements) like `::placeholder`, `::color-swatch`, `::field-text`, etc. See [this docs](https://hackmd.io/@ChaKweTiau/BJ3zRdLQlg) for more details of the implementation. Then, the implemented pseudo element is utilized to implement style matching for input `type=text`. Servo's side of: https://github.com/servo/stylo/pull/212 Testing: No WPT regression. --------- Signed-off-by: stevennovaryo --- Cargo.lock | 24 ++--- components/layout/stylesheets/servo.css | 9 ++ components/script/dom/element.rs | 37 +++++++ components/script/dom/htmldetailselement.rs | 17 +-- components/script/dom/htmlinputelement.rs | 55 ++-------- components/script/dom/htmlmediaelement.rs | 20 +--- components/script/dom/htmlmeterelement.rs | 17 +-- components/script/dom/htmlprogresselement.rs | 17 +-- components/script/dom/htmlselectelement.rs | 17 +-- components/script/dom/node.rs | 40 ++++++- components/script/dom/raredata.rs | 8 ++ components/script/layout_dom/element.rs | 103 ++++++++++++++----- 12 files changed, 201 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df29d426051..ea600f9ab04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7249,7 +7249,7 @@ dependencies = [ [[package]] name = "selectors" version = "0.30.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "bitflags 2.9.1", "cssparser", @@ -7555,7 +7555,7 @@ dependencies = [ [[package]] name = "servo_arc" version = "0.4.1" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "serde", "stable_deref_trait", @@ -8017,7 +8017,7 @@ dependencies = [ [[package]] name = "stylo" version = "0.5.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "app_units", "arrayvec", @@ -8074,7 +8074,7 @@ dependencies = [ [[package]] name = "stylo_atoms" version = "0.5.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "string_cache", "string_cache_codegen", @@ -8083,12 +8083,12 @@ dependencies = [ [[package]] name = "stylo_config" version = "0.5.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" [[package]] name = "stylo_derive" version = "0.5.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "darling", "proc-macro2", @@ -8100,7 +8100,7 @@ dependencies = [ [[package]] name = "stylo_dom" version = "0.5.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "bitflags 2.9.1", "stylo_malloc_size_of", @@ -8109,7 +8109,7 @@ dependencies = [ [[package]] name = "stylo_malloc_size_of" version = "0.5.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "app_units", "cssparser", @@ -8126,12 +8126,12 @@ dependencies = [ [[package]] name = "stylo_static_prefs" version = "0.5.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" [[package]] name = "stylo_traits" version = "0.5.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "app_units", "bitflags 2.9.1", @@ -8548,7 +8548,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "to_shmem" version = "0.2.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "cssparser", "servo_arc", @@ -8561,7 +8561,7 @@ dependencies = [ [[package]] name = "to_shmem_derive" version = "0.1.0" -source = "git+https://github.com/servo/stylo?branch=2025-07-01#ece49719ac02599750bd15901ecd7be24e96a26a" +source = "git+https://github.com/servo/stylo?branch=2025-07-01#673d9632edcec46250cfea9094eaa841f5dc8526" dependencies = [ "darling", "proc-macro2", diff --git a/components/layout/stylesheets/servo.css b/components/layout/stylesheets/servo.css index cb206bbcd00..c3ad5b77e87 100644 --- a/components/layout/stylesheets/servo.css +++ b/components/layout/stylesheets/servo.css @@ -19,6 +19,15 @@ textarea { font-size: 0.8333em; } +input::color-swatch { + width: 100%; + height: 100%; + display: inline-block; + box-sizing: border-box; + border: 1px solid gray; + border-radius: 2px; +} + input::selection, textarea::selection { background: rgba(176, 214, 255, 1.0); diff --git a/components/script/dom/element.rs b/components/script/dom/element.rs index bc43471686c..6a457f58784 100644 --- a/components/script/dom/element.rs +++ b/components/script/dom/element.rs @@ -687,6 +687,43 @@ impl Element { Ok(shadow_root) } + /// Attach a UA widget shadow root with its default parameters. + /// Additionally mark ShadowRoot to use styling configuration for a UA widget. + /// + /// The general trait of these elements is that it would hide the implementation. + /// Thus, we would make it inaccessible (i.e., closed mode, not cloneable, and + /// not serializable). + /// + /// With UA shadow root element being assumed as one element, any focus should + /// be delegated to its host. + /// + // TODO: Ideally, all of the UA shadow root should use UA widget styling, but + // some of the UA widget implemented prior to the implementation of Gecko's + // UA widget matching might need some tweaking. + // FIXME: We are yet to implement more complex focusing with that is necessary + // for delegate focus, and we are using workarounds for that right now. + pub(crate) fn attach_ua_shadow_root( + &self, + use_ua_widget_styling: bool, + can_gc: CanGc, + ) -> DomRoot { + let root = self + .attach_shadow( + IsUserAgentWidget::Yes, + ShadowRootMode::Closed, + false, + false, + false, + SlotAssignmentMode::Manual, + can_gc, + ) + .expect("Attaching UA shadow root failed"); + + root.upcast::() + .set_in_ua_widget(use_ua_widget_styling); + root + } + pub(crate) fn detach_shadow(&self, can_gc: CanGc) { let Some(ref shadow_root) = self.shadow_root() else { unreachable!("Trying to detach a non-attached shadow root"); diff --git a/components/script/dom/htmldetailselement.rs b/components/script/dom/htmldetailselement.rs index 95fc6ad907e..17cdc9f966b 100644 --- a/components/script/dom/htmldetailselement.rs +++ b/components/script/dom/htmldetailselement.rs @@ -13,9 +13,6 @@ use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::HTMLDetailsElementBinding::HTMLDetailsElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLSlotElementBinding::HTMLSlotElement_Binding::HTMLSlotElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; -use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ - ShadowRootMode, SlotAssignmentMode, -}; use crate::dom::bindings::codegen::UnionTypes::ElementOrText; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::refcounted::Trusted; @@ -26,7 +23,6 @@ use crate::dom::eventtarget::EventTarget; use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlslotelement::HTMLSlotElement; use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeDamage, NodeTraits}; -use crate::dom::shadowroot::IsUserAgentWidget; use crate::dom::text::Text; use crate::dom::virtualmethods::VirtualMethods; use crate::script_runtime::CanGc; @@ -103,18 +99,11 @@ impl HTMLDetailsElement { fn create_shadow_tree(&self, can_gc: CanGc) { let document = self.owner_document(); + // TODO(stevennovaryo): Reimplement details styling so that it would not + // mess the cascading and require some reparsing. let root = self .upcast::() - .attach_shadow( - IsUserAgentWidget::Yes, - ShadowRootMode::Closed, - false, - false, - false, - SlotAssignmentMode::Manual, - can_gc, - ) - .expect("Attaching UA shadow root failed"); + .attach_ua_shadow_root(false, can_gc); let summary = HTMLSlotElement::new(local_name!("slot"), None, &document, None, can_gc); root.upcast::() diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index b5386ad8d3a..c016f003295 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -29,10 +29,8 @@ use js::rust::{HandleObject, MutableHandleObject}; use net_traits::blob_url_store::get_blob_origin; use net_traits::filemanager_thread::FileManagerThreadMsg; use net_traits::{CoreResourceMsg, IpcSend}; -use script_bindings::codegen::GenericBindings::ShadowRootBinding::{ - ShadowRootMode, SlotAssignmentMode, -}; use style::attr::AttrValue; +use style::selector_parser::PseudoElement; use style::str::{split_commas, str_join}; use stylo_atoms::Atom; use stylo_dom::ElementState; @@ -59,7 +57,7 @@ use crate::dom::bindings::str::{DOMString, FromInputValueString, ToInputValueStr use crate::dom::clipboardevent::ClipboardEvent; use crate::dom::compositionevent::CompositionEvent; use crate::dom::document::Document; -use crate::dom::element::{AttributeMutation, Element, ElementCreator, LayoutElementHelpers}; +use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers}; use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; use crate::dom::file::File; @@ -73,14 +71,13 @@ use crate::dom::htmlformelement::{ FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, ResetFrom, SubmittedFrom, }; -use crate::dom::htmlstyleelement::HTMLStyleElement; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::mouseevent::MouseEvent; use crate::dom::node::{ BindContext, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext, }; use crate::dom::nodelist::NodeList; -use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot}; +use crate::dom::shadowroot::ShadowRoot; use crate::dom::textcontrol::{TextControlElement, TextControlSelection}; use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor}; use crate::dom::validitystate::{ValidationFlags, ValidityState}; @@ -119,16 +116,6 @@ enum ShadowTree { // TODO: Add shadow trees for other input types (range etc) here } -const COLOR_TREE_STYLE: &str = " -#color-value { - width: 100%; - height: 100%; - box-sizing: border-box; - border: 1px solid gray; - border-radius: 2px; -} -"; - /// #[derive(Clone, Copy, Default, JSTraceable, PartialEq)] #[allow(dead_code)] @@ -1064,19 +1051,9 @@ impl HTMLInputElement { /// Return a reference to the ShadowRoot that this element is a host of, /// or create one if none exists. fn shadow_root(&self, can_gc: CanGc) -> DomRoot { - self.upcast::().shadow_root().unwrap_or_else(|| { - self.upcast::() - .attach_shadow( - IsUserAgentWidget::Yes, - ShadowRootMode::Closed, - false, - false, - false, - SlotAssignmentMode::Manual, - can_gc, - ) - .expect("Attaching UA shadow root failed") - }) + self.upcast::() + .shadow_root() + .unwrap_or_else(|| self.upcast::().attach_ua_shadow_root(true, can_gc)) } fn create_color_shadow_tree(&self, can_gc: CanGc) { @@ -1085,29 +1062,13 @@ impl HTMLInputElement { Node::replace_all(None, shadow_root.upcast::(), can_gc); let color_value = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); - color_value - .upcast::() - .SetId(DOMString::from("color-value"), can_gc); shadow_root .upcast::() .AppendChild(color_value.upcast::(), can_gc) .unwrap(); - - let style = HTMLStyleElement::new( - local_name!("style"), - None, - &document, - None, - ElementCreator::ScriptCreated, - can_gc, - ); - style + color_value .upcast::() - .SetTextContent(Some(DOMString::from(COLOR_TREE_STYLE)), can_gc); - shadow_root - .upcast::() - .AppendChild(style.upcast::(), can_gc) - .unwrap(); + .set_implemented_pseudo_element(PseudoElement::ColorSwatch); let _ = self .shadow_tree diff --git a/components/script/dom/htmlmediaelement.rs b/components/script/dom/htmlmediaelement.rs index 32b2dd3e8a8..e8dfabec367 100644 --- a/components/script/dom/htmlmediaelement.rs +++ b/components/script/dom/htmlmediaelement.rs @@ -56,9 +56,6 @@ use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorConsta use crate::dom::bindings::codegen::Bindings::MediaErrorBinding::MediaErrorMethods; use crate::dom::bindings::codegen::Bindings::NavigatorBinding::Navigator_Binding::NavigatorMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; -use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ - ShadowRootMode, SlotAssignmentMode, -}; use crate::dom::bindings::codegen::Bindings::TextTrackBinding::{TextTrackKind, TextTrackMode}; use crate::dom::bindings::codegen::Bindings::URLBinding::URLMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods; @@ -93,7 +90,6 @@ use crate::dom::mediastream::MediaStream; use crate::dom::node::{Node, NodeDamage, NodeTraits, UnbindContext}; use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::promise::Promise; -use crate::dom::shadowroot::IsUserAgentWidget; use crate::dom::texttrack::TextTrack; use crate::dom::texttracklist::TextTrackList; use crate::dom::timeranges::{TimeRanges, TimeRangesContainer}; @@ -2020,17 +2016,11 @@ impl HTMLMediaElement { // if we are already showing the controls. return; } - let shadow_root = element - .attach_shadow( - IsUserAgentWidget::Yes, - ShadowRootMode::Closed, - false, - false, - false, - SlotAssignmentMode::Manual, - can_gc, - ) - .unwrap(); + // FIXME(stevennovaryo): Recheck styling of media element to avoid + // reparsing styles. + let shadow_root = self + .upcast::() + .attach_ua_shadow_root(false, can_gc); let document = self.owner_document(); let script = HTMLScriptElement::new( local_name!("script"), diff --git a/components/script/dom/htmlmeterelement.rs b/components/script/dom/htmlmeterelement.rs index 5e816e56d01..51d721ccb23 100644 --- a/components/script/dom/htmlmeterelement.rs +++ b/components/script/dom/htmlmeterelement.rs @@ -14,9 +14,6 @@ use crate::dom::attr::Attr; use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::HTMLMeterElementBinding::HTMLMeterElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; -use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ - ShadowRootMode, SlotAssignmentMode, -}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::num::Finite; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; @@ -27,7 +24,6 @@ use crate::dom::htmldivelement::HTMLDivElement; use crate::dom::htmlelement::HTMLElement; use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeTraits}; use crate::dom::nodelist::NodeList; -use crate::dom::shadowroot::IsUserAgentWidget; use crate::dom::virtualmethods::VirtualMethods; use crate::script_runtime::CanGc; @@ -79,18 +75,7 @@ impl HTMLMeterElement { fn create_shadow_tree(&self, can_gc: CanGc) { let document = self.owner_document(); - let root = self - .upcast::() - .attach_shadow( - IsUserAgentWidget::Yes, - ShadowRootMode::Closed, - false, - false, - false, - SlotAssignmentMode::Manual, - can_gc, - ) - .expect("Attaching UA shadow root failed"); + let root = self.upcast::().attach_ua_shadow_root(true, can_gc); let meter_value = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); root.upcast::() diff --git a/components/script/dom/htmlprogresselement.rs b/components/script/dom/htmlprogresselement.rs index 71b8c4968c2..c6707daa0c4 100644 --- a/components/script/dom/htmlprogresselement.rs +++ b/components/script/dom/htmlprogresselement.rs @@ -13,9 +13,6 @@ use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::codegen::Bindings::ElementBinding::Element_Binding::ElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLProgressElementBinding::HTMLProgressElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::Node_Binding::NodeMethods; -use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ - ShadowRootMode, SlotAssignmentMode, -}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::num::Finite; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; @@ -26,7 +23,6 @@ use crate::dom::htmldivelement::HTMLDivElement; use crate::dom::htmlelement::HTMLElement; use crate::dom::node::{BindContext, Node, NodeTraits}; use crate::dom::nodelist::NodeList; -use crate::dom::shadowroot::IsUserAgentWidget; use crate::dom::virtualmethods::VirtualMethods; use crate::script_runtime::CanGc; @@ -77,18 +73,7 @@ impl HTMLProgressElement { fn create_shadow_tree(&self, can_gc: CanGc) { let document = self.owner_document(); - let root = self - .upcast::() - .attach_shadow( - IsUserAgentWidget::Yes, - ShadowRootMode::Closed, - false, - false, - false, - SlotAssignmentMode::Manual, - can_gc, - ) - .expect("Attaching UA shadow root failed"); + let root = self.upcast::().attach_ua_shadow_root(true, can_gc); let progress_bar = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); // FIXME: This should use ::-moz-progress-bar diff --git a/components/script/dom/htmlselectelement.rs b/components/script/dom/htmlselectelement.rs index 2fd8f35c4d8..bb976d32e57 100644 --- a/components/script/dom/htmlselectelement.rs +++ b/components/script/dom/htmlselectelement.rs @@ -28,9 +28,6 @@ use crate::dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptio use crate::dom::bindings::codegen::Bindings::HTMLOptionsCollectionBinding::HTMLOptionsCollectionMethods; use crate::dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods; use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; -use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{ - ShadowRootMode, SlotAssignmentMode, -}; use crate::dom::bindings::codegen::GenericBindings::CharacterDataBinding::CharacterData_Binding::CharacterDataMethods; use crate::dom::bindings::codegen::UnionTypes::{ HTMLElementOrLong, HTMLOptionElementOrHTMLOptGroupElement, @@ -54,7 +51,6 @@ use crate::dom::htmloptionelement::HTMLOptionElement; use crate::dom::htmloptionscollection::HTMLOptionsCollection; use crate::dom::node::{BindContext, ChildrenMutation, Node, NodeTraits, UnbindContext}; use crate::dom::nodelist::NodeList; -use crate::dom::shadowroot::IsUserAgentWidget; use crate::dom::text::Text; use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor}; use crate::dom::validitystate::{ValidationFlags, ValidityState}; @@ -260,18 +256,7 @@ impl HTMLSelectElement { fn create_shadow_tree(&self, can_gc: CanGc) { let document = self.owner_document(); - let root = self - .upcast::() - .attach_shadow( - IsUserAgentWidget::Yes, - ShadowRootMode::Closed, - false, - false, - false, - SlotAssignmentMode::Manual, - can_gc, - ) - .expect("Attaching UA shadow root failed"); + let root = self.upcast::().attach_ua_shadow_root(true, can_gc); let select_box = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); select_box.upcast::().set_string_attribute( diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 11d06386cdb..fc36965c17d 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -48,7 +48,7 @@ use style::attr::AttrValue; use style::context::QuirksMode; use style::dom::OpaqueNode; use style::properties::ComputedValues; -use style::selector_parser::{SelectorImpl, SelectorParser}; +use style::selector_parser::{PseudoElement, SelectorImpl, SelectorParser}; use style::stylesheets::{Stylesheet, UrlExtraData}; use uuid::Uuid; use xml5ever::{local_name, serialize as xml_serialize}; @@ -233,6 +233,10 @@ bitflags! { /// Whether this node has a weird parser insertion mode. i.e whether setting innerHTML /// needs extra work or not const HAS_WEIRD_PARSER_INSERTION_MODE = 1 << 11; + + /// Whether this node resides in UA shadow DOM. Element within UA Shadow DOM + /// will have a different style computation behavior + const IS_IN_UA_WIDGET = 1 << 12; } } @@ -291,6 +295,7 @@ impl Node { let parent_is_in_a_document_tree = self.is_in_a_document_tree(); let parent_in_shadow_tree = self.is_in_a_shadow_tree(); let parent_is_connected = self.is_connected(); + let parent_is_in_ua_widget = self.is_in_ua_widget(); for node in new_child.traverse_preorder(ShadowIncluding::No) { if parent_in_shadow_tree { @@ -305,6 +310,7 @@ impl Node { ); node.set_flag(NodeFlags::IS_IN_SHADOW_TREE, parent_in_shadow_tree); node.set_flag(NodeFlags::IS_CONNECTED, parent_is_connected); + node.set_flag(NodeFlags::IS_IN_UA_WIDGET, parent_is_in_ua_widget); // Out-of-document elements never have the descendants flag set. debug_assert!(!node.get_flag(NodeFlags::HAS_DIRTY_DESCENDANTS)); @@ -696,6 +702,14 @@ impl Node { self.flags.get().contains(NodeFlags::IS_CONNECTED) } + pub(crate) fn set_in_ua_widget(&self, in_ua_widget: bool) { + self.set_flag(NodeFlags::IS_IN_UA_WIDGET, in_ua_widget) + } + + pub(crate) fn is_in_ua_widget(&self) -> bool { + self.flags.get().contains(NodeFlags::IS_IN_UA_WIDGET) + } + /// Returns the type ID of this node. pub(crate) fn type_id(&self) -> NodeTypeId { match *self.eventtarget.type_id() { @@ -1546,6 +1560,20 @@ impl Node { next_node: move |n| n.parent_in_flat_tree(), } } + + /// We are marking this as an implemented pseudo element. + pub(crate) fn set_implemented_pseudo_element(&self, pseudo_element: PseudoElement) { + // Implemented pseudo element should exist only in the UA shadow DOM. + debug_assert!(self.is_in_ua_widget()); + self.ensure_rare_data().implemented_pseudo_element = Some(pseudo_element); + } + + pub(crate) fn implemented_pseudo_element(&self) -> Option { + self.rare_data + .borrow() + .as_ref() + .and_then(|rare_data| rare_data.implemented_pseudo_element) + } } /// Iterate through `nodes` until we find a `Node` that is not in `not_in` @@ -1630,6 +1658,8 @@ pub(crate) trait LayoutNodeHelpers<'dom> { fn iframe_browsing_context_id(self) -> Option; fn iframe_pipeline_id(self) -> Option; fn opaque(self) -> OpaqueNode; + fn implemented_pseudo_element(&self) -> Option; + fn is_in_ua_widget(&self) -> bool; } impl<'dom> LayoutDom<'dom, Node> { @@ -1880,6 +1910,14 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> { fn opaque(self) -> OpaqueNode { unsafe { OpaqueNode(self.get_jsobject() as usize) } } + + fn implemented_pseudo_element(&self) -> Option { + self.unsafe_get().implemented_pseudo_element() + } + + fn is_in_ua_widget(&self) -> bool { + self.unsafe_get().is_in_ua_widget() + } } // diff --git a/components/script/dom/raredata.rs b/components/script/dom/raredata.rs index 2c303d6874f..c4624975558 100644 --- a/components/script/dom/raredata.rs +++ b/components/script/dom/raredata.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use euclid::default::Rect; +use style::selector_parser::PseudoElement; use stylo_atoms::Atom; use crate::dom::bindings::root::{Dom, MutNullableDom}; @@ -47,6 +48,13 @@ pub(crate) struct NodeRareData { /// The live list of children return by .childNodes. pub(crate) child_list: MutNullableDom, + + /// Whether this node represents a certain implemented pseudo-element. + /// An implemented pseudo-element is a real element within a UA shadow tree + /// that will match a certain pseudo-element selector. + /// An example of this is the element matching the `::placeholder` selector. + #[no_trace] + pub(crate) implemented_pseudo_element: Option, } #[derive(Default, JSTraceable, MallocSizeOf)] diff --git a/components/script/layout_dom/element.rs b/components/script/layout_dom/element.rs index c5cbd563fcc..7675eb247fd 100644 --- a/components/script/layout_dom/element.rs +++ b/components/script/layout_dom/element.rs @@ -201,6 +201,22 @@ impl<'dom> style::dom::TElement for ServoLayoutElement<'dom> { self.as_node().traversal_parent() } + fn inheritance_parent(&self) -> Option { + 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(); + } + + // FIXME: By default the inheritance parent would be the Self::parent_element + // but probably we should use the flattened tree parent. + self.parent_element() + } + fn is_html_element(&self) -> bool { ServoLayoutElement::is_html_element(self) } @@ -355,6 +371,21 @@ impl<'dom> style::dom::TElement for ServoLayoutElement<'dom> { .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 { + self.as_node().node.implemented_pseudo_element() + } + fn store_children_to_process(&self, n: isize) { let data = self.get_style_data().unwrap(); data.parallel @@ -585,6 +616,18 @@ impl<'dom> ::selectors::Element for ServoLayoutElement<'dom> { 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 { + debug_assert!(self.is_pseudo_element()); + debug_assert!(!self.matches_user_and_content_rules()); + self.containing_shadow_host() + } + fn prev_sibling_element(&self) -> Option { let mut node = self.as_node(); while let Some(sibling) = node.prev_sibling() { @@ -663,18 +706,6 @@ impl<'dom> ::selectors::Element for ServoLayoutElement<'dom> { self.element.namespace() == other.element.namespace() } - fn is_pseudo_element(&self) -> bool { - false - } - - fn match_pseudo_element( - &self, - _pseudo: &PseudoElement, - _context: &mut MatchingContext, - ) -> bool { - false - } - fn match_non_ts_pseudo_class( &self, pseudo_class: &NonTSPseudoClass, @@ -733,6 +764,14 @@ impl<'dom> ::selectors::Element for ServoLayoutElement<'dom> { } } + fn match_pseudo_element( + &self, + pseudo: &PseudoElement, + _context: &mut MatchingContext, + ) -> bool { + self.implemented_pseudo_element() == Some(*pseudo) + } + #[inline] fn is_link(&self) -> bool { match self.as_node().script_type_id() { @@ -934,21 +973,33 @@ impl ::selectors::Element for ServoThreadSafeLayoutElement<'_> { ::selectors::OpaqueElement::new(unsafe { &*(self.as_node().opaque().0 as *const ()) }) } - fn is_pseudo_element(&self) -> bool { - false - } - fn parent_element(&self) -> Option { warn!("ServoThreadSafeLayoutElement::parent_element called"); None } + #[inline] fn parent_node_is_shadow_root(&self) -> bool { - false + self.element.parent_node_is_shadow_root() } + #[inline] fn containing_shadow_host(&self) -> Option { - None + 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.element + .pseudo_element_originating_element() + .and_then(|element| element.as_node().to_threadsafe().as_element()) } // Skips non-element nodes @@ -993,14 +1044,6 @@ impl ::selectors::Element for ServoThreadSafeLayoutElement<'_> { self.element.namespace() == other.element.namespace() } - fn match_pseudo_element( - &self, - _pseudo: &PseudoElement, - _context: &mut MatchingContext, - ) -> bool { - false - } - fn attr_matches( &self, ns: &NamespaceConstraint<&style::Namespace>, @@ -1030,6 +1073,14 @@ impl ::selectors::Element for ServoThreadSafeLayoutElement<'_> { false } + fn match_pseudo_element( + &self, + pseudo: &PseudoElement, + context: &mut MatchingContext, + ) -> bool { + self.element.match_pseudo_element(pseudo, context) + } + fn is_link(&self) -> bool { warn!("ServoThreadSafeLayoutElement::is_link called"); false