From 1a3dd44f78e580dfd0838e1429a432d8eb24e97e Mon Sep 17 00:00:00 2001 From: stevennovaryo Date: Thu, 5 Jun 2025 19:20:08 +0800 Subject: [PATCH] Add internal pseudo element for input Signed-off-by: stevennovaryo --- Cargo.lock | 24 +++---- Cargo.toml | 18 ++--- components/layout/dom_traversal.rs | 15 +++- components/layout/stylesheets/servo.css | 26 +++++++ components/script/dom/htmlinputelement.rs | 85 ++++++----------------- components/script/dom/node.rs | 15 +++- components/script/dom/raredata.rs | 5 ++ components/script/layout_dom/node.rs | 17 ++++- 8 files changed, 114 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3a37a9c4e1..fd5ce80a8a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6607,7 +6607,7 @@ dependencies = [ [[package]] name = "selectors" version = "0.28.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "bitflags 2.9.1", "cssparser", @@ -6902,7 +6902,7 @@ dependencies = [ [[package]] name = "servo_arc" version = "0.4.1" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "serde", "stable_deref_trait", @@ -7363,7 +7363,7 @@ dependencies = [ [[package]] name = "stylo" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "app_units", "arrayvec", @@ -7421,7 +7421,7 @@ dependencies = [ [[package]] name = "stylo_atoms" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "string_cache", "string_cache_codegen", @@ -7430,12 +7430,12 @@ dependencies = [ [[package]] name = "stylo_config" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" [[package]] name = "stylo_derive" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "darling", "proc-macro2", @@ -7447,7 +7447,7 @@ dependencies = [ [[package]] name = "stylo_dom" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "bitflags 2.9.1", "stylo_malloc_size_of", @@ -7456,7 +7456,7 @@ dependencies = [ [[package]] name = "stylo_malloc_size_of" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "app_units", "cssparser", @@ -7473,12 +7473,12 @@ dependencies = [ [[package]] name = "stylo_static_prefs" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" [[package]] name = "stylo_traits" version = "0.3.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "app_units", "bitflags 2.9.1", @@ -7887,7 +7887,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "to_shmem" version = "0.2.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "cssparser", "servo_arc", @@ -7900,7 +7900,7 @@ dependencies = [ [[package]] name = "to_shmem_derive" version = "0.1.0" -source = "git+https://github.com/servo/stylo?branch=2025-05-01#945b70e9a1984cd44ee56b7a674c302b19a4f620" +source = "git+https://github.com/stevennovaryo/stylo?branch=input-text-internal-pseudo-selector#b0123d478e727125f8f6862e7afb5cf8ebf8ba17" dependencies = [ "darling", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 0fc9d90bf14..c6b4fe33db5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -119,7 +119,7 @@ rustls-pemfile = "2.0" rustls-pki-types = "1.12" script_layout_interface = { path = "components/shared/script_layout" } script_traits = { path = "components/shared/script" } -selectors = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +selectors = { git = "https://github.com/stevennovaryo/stylo", branch = "input-text-internal-pseudo-selector" } serde = "1.0.219" serde_bytes = "0.11" serde_json = "1.0" @@ -127,7 +127,7 @@ servo-media = { git = "https://github.com/servo/media" } servo-media-dummy = { git = "https://github.com/servo/media" } servo-media-gstreamer = { git = "https://github.com/servo/media" } servo-tracing = { path = "components/servo_tracing" } -servo_arc = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +servo_arc = { git = "https://github.com/stevennovaryo/stylo", branch = "input-text-internal-pseudo-selector" } smallbitvec = "2.6.0" smallvec = "1.15" snapshot = { path = "./components/shared/snapshot" } @@ -136,12 +136,12 @@ string_cache = "0.8" string_cache_codegen = "0.5" strum = "0.26" strum_macros = "0.26" -stylo = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_atoms = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_config = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_dom = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_malloc_size_of = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } -stylo_traits = { git = "https://github.com/servo/stylo", branch = "2025-05-01" } +stylo = { git = "https://github.com/stevennovaryo/stylo", branch = "input-text-internal-pseudo-selector" } +stylo_atoms = { git = "https://github.com/stevennovaryo/stylo", branch = "input-text-internal-pseudo-selector" } +stylo_config = { git = "https://github.com/stevennovaryo/stylo", branch = "input-text-internal-pseudo-selector" } +stylo_dom = { git = "https://github.com/stevennovaryo/stylo", branch = "input-text-internal-pseudo-selector" } +stylo_malloc_size_of = { git = "https://github.com/stevennovaryo/stylo", branch = "input-text-internal-pseudo-selector" } +stylo_traits = { git = "https://github.com/stevennovaryo/stylo", branch = "input-text-internal-pseudo-selector" } surfman = { git = "https://github.com/servo/surfman", rev = "f7688b4585f9e0b5d4bf8ee8e4a91e82349610b1", features = ["chains"] } syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] } synstructure = "0.13" @@ -233,7 +233,7 @@ codegen-units = 1 # stylo_dom = { path = "../stylo/stylo_dom" } # stylo_malloc_size_of = { path = "../stylo/malloc_size_of" } # stylo_traits = { path = "../stylo/style_traits" } -# +# # # Or for WebRender: # # [patch."https://github.com/servo/webrender"] diff --git a/components/layout/dom_traversal.rs b/components/layout/dom_traversal.rs index 4da6aa48b55..f6e8ec71458 100644 --- a/components/layout/dom_traversal.rs +++ b/components/layout/dom_traversal.rs @@ -88,7 +88,20 @@ impl<'dom> NodeAndStyleInfo<'dom> { } pub(crate) fn get_selected_style(&self) -> ServoArc { - self.node.to_threadsafe().selected_style() + // The Selection of an inner editor of a `` should follow its + // Shadow Host's selection style. + if self.node.is_text_control_inner_editor() { + self.node + .as_element() + .expect("Inner editor should be an element") + .containing_shadow_host() + .expect("Unassociated inner editor") + .as_node() + .to_threadsafe() + .selected_style() + } else { + self.node.to_threadsafe().selected_style() + } } pub(crate) fn get_selection_range(&self) -> Option> { diff --git a/components/layout/stylesheets/servo.css b/components/layout/stylesheets/servo.css index cb206bbcd00..a77e0dd7b4d 100644 --- a/components/layout/stylesheets/servo.css +++ b/components/layout/stylesheets/servo.css @@ -19,6 +19,32 @@ textarea { font-size: 0.8333em; } +::-servo-text-control-inner-editor { + overflow-wrap: normal; + pointer-events: auto; +} + +::-servo-text-control-inner-container { + position: relative; + height: 100%; + pointer-events: none; + display: flex; +} + +::-servo-text-control-inner-editor, ::-servo-text-control-placeholder { + white-space: pre; + margin-block: auto !important; + inset-block: 0 !important; + block-size: fit-content !important; +} + +::-servo-text-control-placeholder { + overflow: hidden !important; + position: absolute !important; + color: grey !important; + pointer-events: none !important; +} + input::selection, textarea::selection { background: rgba(176, 214, 255, 1.0); diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index ee61421dfe8..63691beddbf 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -33,6 +33,7 @@ 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; @@ -134,44 +135,6 @@ struct InputTypeColorShadowTree { color_value: Dom, } -// FIXME: These styles should be inside UA stylesheet, but it is not possible without internal pseudo element support. -const TEXT_TREE_STYLE: &str = " -#input-editor::selection { - background: rgba(176, 214, 255, 1.0); - color: black; -} - -:host:not(:placeholder-shown) #input-placeholder { - visibility: hidden !important -} - -#input-editor { - overflow-wrap: normal; - pointer-events: auto; -} - -#input-container { - position: relative; - height: 100%; - pointer-events: none; - display: flex; -} - -#input-editor, #input-placeholder { - white-space: pre; - margin-block: auto !important; - inset-block: 0 !important; - block-size: fit-content !important; -} - -#input-placeholder { - overflow: hidden !important; - position: absolute !important; - color: grey; - pointer-events: none !important; -} -"; - #[derive(Clone, JSTraceable, MallocSizeOf)] #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] #[non_exhaustive] @@ -1149,8 +1112,8 @@ impl HTMLInputElement { let inner_container = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); inner_container - .upcast::() - .SetId(DOMString::from("input-container"), can_gc); + .upcast::() + .set_pseudo_element(PseudoElement::ServoTextControlInnerContainer); shadow_root .upcast::() .AppendChild(inner_container.upcast::(), can_gc) @@ -1159,8 +1122,8 @@ impl HTMLInputElement { let placeholder_container = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); placeholder_container - .upcast::() - .SetId(DOMString::from("input-placeholder"), can_gc); + .upcast::() + .set_pseudo_element(PseudoElement::ServoTextControlPlaceholder); inner_container .upcast::() .AppendChild(placeholder_container.upcast::(), can_gc) @@ -1168,8 +1131,8 @@ impl HTMLInputElement { let text_container = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); text_container - .upcast::() - .SetId(DOMString::from("input-editor"), can_gc); + .upcast::() + .set_pseudo_element(PseudoElement::ServoTextControlInnerEditor); text_container .upcast::() .set_text_control_inner_editor(); @@ -1178,24 +1141,6 @@ impl HTMLInputElement { .AppendChild(text_container.upcast::(), can_gc) .unwrap(); - let style = HTMLStyleElement::new( - local_name!("style"), - None, - &document, - None, - ElementCreator::ScriptCreated, - can_gc, - ); - // TODO(stevennovaryo): Either use UA stylesheet with internal pseudo element or preemptively parse - // the stylesheet to reduce the costly operation and avoid CSP related error. - style - .upcast::() - .SetTextContent(Some(DOMString::from(TEXT_TREE_STYLE)), can_gc); - shadow_root - .upcast::() - .AppendChild(style.upcast::(), can_gc) - .unwrap(); - let _ = self .shadow_tree .borrow_mut() @@ -1662,6 +1607,7 @@ impl HTMLInputElementMethods for HTMLInputElement { // normally being done in the attributed mutated. And, being // done in another scope to prevent borrow checker issues. self.update_placeholder_shown_state(); + self.update_text_shadow_tree_placeholder(can_gc); }, ValueMode::Default | ValueMode::DefaultOn => { self.upcast::() @@ -2251,10 +2197,17 @@ impl HTMLInputElement { return; } + // Ideally we are not supposed to handle visibility of the placeholder. + // But we are doing so because UA stylesheet does not support `:host` selector yet. + let placeholder_text = match self.upcast::().placeholder_shown_state() { + true => self.placeholder.borrow().clone(), + false => "".into(), + }; + self.text_shadow_tree(can_gc) .placeholder_container .upcast::() - .SetTextContent(Some(self.placeholder.borrow().clone()), can_gc); + .SetTextContent(Some(placeholder_text), can_gc); } // https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file) @@ -2882,8 +2835,8 @@ impl VirtualMethods for HTMLInputElement { }, } - self.update_text_shadow_tree_placeholder(can_gc); self.update_placeholder_shown_state(); + self.update_text_shadow_tree_placeholder(can_gc); }, // FIXME(stevennovaryo): This is only reachable by Default and DefaultOn value mode. While others // are being handled in [Self::SetValue]. Should we merge this two together? @@ -2894,6 +2847,7 @@ impl VirtualMethods for HTMLInputElement { self.sanitize_value(&mut value); self.textinput.borrow_mut().set_content(value); self.update_placeholder_shown_state(); + self.update_text_shadow_tree_placeholder(can_gc); self.upcast::().dirty(NodeDamage::OtherNodeDamage); }, @@ -2935,8 +2889,8 @@ impl VirtualMethods for HTMLInputElement { .extend(attr.value().chars().filter(|&c| c != '\n' && c != '\r')); } } - self.update_text_shadow_tree_placeholder(can_gc); self.update_placeholder_shown_state(); + self.update_text_shadow_tree_placeholder(can_gc); }, local_name!("readonly") => { if self.input_type().is_textual() { @@ -3094,6 +3048,7 @@ impl VirtualMethods for HTMLInputElement { } self.value_dirty.set(true); self.update_placeholder_shown_state(); + self.update_text_shadow_tree_placeholder(can_gc); self.upcast::().dirty(NodeDamage::OtherNodeDamage); event.mark_as_handled(); }, diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index ab814bc9108..fb32228ab78 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -45,7 +45,7 @@ use smallvec::SmallVec; 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}; @@ -1572,6 +1572,10 @@ impl Node { next_node: move |n| n.parent_in_flat_tree(), } } + + pub(crate) fn set_pseudo_element(&self, pseudo_element: PseudoElement) { + self.ensure_rare_data().pseudo_element = Some(pseudo_element); + } } /// Iterate through `nodes` until we find a `Node` that is not in `not_in` @@ -1659,6 +1663,7 @@ pub(crate) trait LayoutNodeHelpers<'dom> { fn iframe_browsing_context_id(self) -> Option; fn iframe_pipeline_id(self) -> Option; fn opaque(self) -> OpaqueNode; + fn pseudo_element(&self) -> Option; } impl<'dom> LayoutDom<'dom, Node> { @@ -1931,6 +1936,14 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> { fn opaque(self) -> OpaqueNode { unsafe { OpaqueNode(self.get_jsobject() as usize) } } + + #[allow(unsafe_code)] + fn pseudo_element(&self) -> Option { + self.unsafe_get() + .rare_data() + .as_ref() + .and_then(|data| data.pseudo_element) + } } // diff --git a/components/script/dom/raredata.rs b/components/script/dom/raredata.rs index 0c048956217..a036b70a6a0 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}; @@ -46,6 +47,10 @@ pub(crate) struct NodeRareData { /// The live list of children return by .childNodes. pub(crate) child_list: MutNullableDom, + + /// Internal Pseudo Element type + #[no_trace] + pub(crate) pseudo_element: Option, } #[derive(Default, JSTraceable, MallocSizeOf)] diff --git a/components/script/layout_dom/node.rs b/components/script/layout_dom/node.rs index 685fd8b9333..d1cf60df9ce 100644 --- a/components/script/layout_dom/node.rs +++ b/components/script/layout_dom/node.rs @@ -244,7 +244,10 @@ pub struct ServoThreadSafeLayoutNode<'dom> { impl<'dom> ServoThreadSafeLayoutNode<'dom> { /// Creates a new `ServoThreadSafeLayoutNode` from the given `ServoLayoutNode`. pub fn new(node: ServoLayoutNode<'dom>) -> Self { - ServoThreadSafeLayoutNode { node, pseudo: None } + ServoThreadSafeLayoutNode { + node, + pseudo: node.node.pseudo_element(), + } } /// Returns the interior of this node as a `LayoutDom`. This is highly unsafe for layout to @@ -440,13 +443,18 @@ pub struct ServoThreadSafeLayoutNodeChildrenIterator<'dom> { } impl<'dom> ServoThreadSafeLayoutNodeChildrenIterator<'dom> { + // MYNOTES: This seems to be unused anywhere. pub fn new(parent: ServoThreadSafeLayoutNode<'dom>) -> Self { let first_child = match parent.pseudo_element() { None => parent .with_pseudo(PseudoElement::Before) .or_else(|| parent.with_pseudo(PseudoElement::DetailsSummary)) .or_else(|| unsafe { parent.dangerous_first_child() }), - Some(PseudoElement::DetailsContent) | Some(PseudoElement::DetailsSummary) => unsafe { + Some(PseudoElement::DetailsContent) | + Some(PseudoElement::DetailsSummary) | + Some(PseudoElement::ServoTextControlInnerContainer) | + Some(PseudoElement::ServoTextControlInnerEditor) | + Some(PseudoElement::ServoTextControlPlaceholder) => unsafe { parent.dangerous_first_child() }, _ => None, @@ -516,7 +524,10 @@ impl<'dom> Iterator for ServoThreadSafeLayoutNodeChildrenIterator<'dom> { Some(PseudoElement::DetailsSummary) => { self.parent_node.with_pseudo(PseudoElement::DetailsContent) }, - Some(PseudoElement::DetailsContent) => { + Some(PseudoElement::DetailsContent) | + Some(PseudoElement::ServoTextControlInnerContainer) | + Some(PseudoElement::ServoTextControlInnerEditor) | + Some(PseudoElement::ServoTextControlPlaceholder) => { self.parent_node.with_pseudo(PseudoElement::After) }, Some(PseudoElement::After) => None,