From 0026213799a614cf67abe471652a39e80502ec58 Mon Sep 17 00:00:00 2001 From: Jo Steven Novaryo <65610990+stevennovaryo@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:43:38 +0800 Subject: [PATCH] dom: Implement textual `` lazy initialized placeholder (#38821) Following #37527, every textual input is constructing the containers for value and placeholder. However not all of textual `` element require the initialization of such placeholder container. This is apparent with JS UI framework that defines its own placeholder management. This PR add lazy initialization for placeholder which construct the relevant HTML elements for placeholder container whenever it is necessary. Testing: Existing WPT coverage Signed-off-by: Jo Steven Novaryo --- components/script/dom/htmlinputelement.rs | 119 ++++++++++++++-------- 1 file changed, 77 insertions(+), 42 deletions(-) diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index cf68b60c90c..aa5693ad47d 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -121,11 +121,34 @@ const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen"; // But, this could be slower in performance and does have some discrepancies. For example, // they would try to vertically align text baseline with the baseline of other // TextNode within an inline flow. Another example is the horizontal scroll. -// FIXME(stevennovaryo): Implement lazily initiated placeholder. -// FIXME(stevennovaryo): Refactor these logic into a TextControl wrapper that would handle all textual input. +// FIXME(#38263): Refactor these logics into a TextControl wrapper that would decouple all textual input. struct InputTypeTextShadowTree { + inner_container: Dom, text_container: Dom, - placeholder_container: Dom, + placeholder_container: DomRefCell>>, +} + +impl InputTypeTextShadowTree { + /// Initialize the placeholder container only when it is necessary. This would help the performance of input + /// element with shadow dom that is quite bulky. + fn init_placeholder_container_if_necessary(&self, host: &HTMLInputElement, can_gc: CanGc) { + // If the container is already initialized or there is no placeholder then it is not necessary to + // initialize a new placeholder container. + if self.placeholder_container.borrow().is_some() || host.placeholder.borrow().is_empty() { + return; + } + + *self.placeholder_container.borrow_mut() = Some( + create_ua_widget_div_with_text_node( + &host.owner_document(), + self.inner_container.upcast::(), + PseudoElement::Placeholder, + true, + can_gc, + ) + .as_traced(), + ); + } } #[derive(Clone, JSTraceable, MallocSizeOf)] @@ -147,6 +170,40 @@ enum ShadowTree { // TODO: Add shadow trees for other input types (range etc) here } +/// Create a div element with a text node within an UA Widget and either append or prepend it to +/// the designated parent. This is used to create the text container for input elements. +fn create_ua_widget_div_with_text_node( + document: &Document, + parent: &Node, + implemented_pseudo: PseudoElement, + as_first_child: bool, + can_gc: CanGc, +) -> DomRoot { + let el = HTMLDivElement::new(local_name!("div"), None, document, None, can_gc); + parent + .upcast::() + .AppendChild(el.upcast::(), can_gc) + .unwrap(); + el.upcast::() + .set_implemented_pseudo_element(implemented_pseudo); + let text_node = document.CreateTextNode("".into(), can_gc); + + if !as_first_child { + el.upcast::() + .AppendChild(text_node.upcast::(), can_gc) + .unwrap(); + } else { + el.upcast::() + .InsertBefore( + text_node.upcast::(), + el.upcast::().GetFirstChild().as_deref(), + can_gc, + ) + .unwrap(); + } + el +} + /// #[derive(Clone, Copy, Debug, Default, JSTraceable, PartialEq)] #[allow(dead_code)] @@ -1106,30 +1163,6 @@ impl HTMLInputElement { .unwrap_or_else(|| self.upcast::().attach_ua_shadow_root(true, can_gc)) } - /// Create a div element with a text node within an UA Widget. - /// This will be used to create the text container for - /// input elements. - fn create_ua_widget_div_with_text_node( - &self, - document: &Document, - parent: &Node, - implemented_pseudo: PseudoElement, - can_gc: CanGc, - ) -> DomRoot { - let el = HTMLDivElement::new(local_name!("div"), None, document, None, can_gc); - parent - .upcast::() - .AppendChild(el.upcast::(), can_gc) - .unwrap(); - el.upcast::() - .set_implemented_pseudo_element(implemented_pseudo); - let text_node = document.CreateTextNode("".into(), can_gc); - el.upcast::() - .AppendChild(text_node.upcast::(), can_gc) - .unwrap(); - el - } - fn create_text_shadow_tree(&self, can_gc: CanGc) { let document = self.owner_document(); let shadow_root = self.shadow_root(can_gc); @@ -1145,17 +1178,11 @@ impl HTMLInputElement { .upcast::() .set_implemented_pseudo_element(PseudoElement::ServoTextControlInnerContainer); - let placeholder_container = self.create_ua_widget_div_with_text_node( - &document, - inner_container.upcast::(), - PseudoElement::Placeholder, - can_gc, - ); - - let text_container = self.create_ua_widget_div_with_text_node( + let text_container = create_ua_widget_div_with_text_node( &document, inner_container.upcast::(), PseudoElement::ServoTextControlInnerEditor, + false, can_gc, ); @@ -1163,8 +1190,9 @@ impl HTMLInputElement { .shadow_tree .borrow_mut() .insert(ShadowTree::Text(InputTypeTextShadowTree { + inner_container: inner_container.as_traced(), text_container: text_container.as_traced(), - placeholder_container: placeholder_container.as_traced(), + placeholder_container: DomRefCell::new(None), })); } @@ -1292,12 +1320,12 @@ impl HTMLInputElement { (true, _) => "\u{200B}".into(), }; - // FIXME(stevennovaryo): Refactor this inside a TextControl wrapper + // We are finding and updating the CharacterData child directly to optimize the update. text_shadow_tree .text_container .upcast::() .GetFirstChild() - .expect("Text container without child") + .expect("UA widget text container without child") .downcast::() .expect("First child is not a CharacterData node") .SetData(value_text); @@ -2263,14 +2291,21 @@ impl HTMLInputElement { return; } + let text_shadow_tree = self.text_shadow_tree(can_gc); + text_shadow_tree.init_placeholder_container_if_necessary(self, can_gc); + + let Some(ref placeholder_container) = *text_shadow_tree.placeholder_container.borrow() + else { + // No update is necesssary. + return; + }; let placeholder_text = self.placeholder.borrow().clone(); - // FIXME(stevennovaryo): Refactor this inside a TextControl wrapper - self.text_shadow_tree(can_gc) - .placeholder_container + // We are finding and updating the CharacterData child directly to optimize the update. + placeholder_container .upcast::() .GetFirstChild() - .expect("Text container without child") + .expect("UA widget text container without child") .downcast::() .expect("First child is not a CharacterData node") .SetData(placeholder_text);