script: Refactors HTMLInputElement.rs into type specific files (#43325)

Refactors HTMLInputElement.rs to split lots of type specific input code
into their own files. These SpecificInputTypes are responsible for their
own shadow trees and also for element specific data such as
FileInputType's filelist.

Testing: Covered by WPTs
Fixes: https://github.com/servo/servo/issues/38263
Fixes: https://github.com/servo/servo/issues/43351

Signed-off-by: Luke Warlow <lwarlow@igalia.com>
Co-authored-by: Oriol Brufau <obrufau@igalia.com>
This commit is contained in:
Luke Warlow
2026-03-26 11:59:22 +00:00
committed by GitHub
parent 75d6338825
commit 087c50657f
39 changed files with 3006 additions and 1674 deletions

View File

@@ -0,0 +1,260 @@
/* 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/. */
use std::cell::Ref;
use html5ever::{local_name, ns};
use js::context::JSContext;
use markup5ever::QualName;
use script_bindings::codegen::GenericBindings::CharacterDataBinding::CharacterDataMethods;
use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
use script_bindings::codegen::GenericBindings::HTMLInputElementBinding::HTMLInputElementMethods;
use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
use script_bindings::inheritance::Castable;
use script_bindings::root::{Dom, DomRoot};
use script_bindings::script_runtime::CanGc;
use style::selector_parser::PseudoElement;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::characterdata::CharacterData;
use crate::dom::document::Document;
use crate::dom::element::{CustomElementCreationMode, Element, ElementCreator};
use crate::dom::htmlinputelement::HTMLInputElement;
use crate::dom::htmlinputelement::input_type::InputType;
use crate::dom::node::{Node, NodeTraits};
const PASSWORD_REPLACEMENT_CHAR: char = '●';
#[derive(Default, JSTraceable, MallocSizeOf, PartialEq)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) struct TextInputWidget {
shadow_tree: DomRefCell<Option<TextInputWidgetShadowTree>>,
}
impl TextInputWidget {
/// Get the shadow tree for this [`HTMLInputElement`], if it is created and valid, otherwise
/// recreate the shadow tree and return it.
fn get_or_create_shadow_tree(
&self,
cx: &mut JSContext,
input: &HTMLInputElement,
) -> Ref<'_, TextInputWidgetShadowTree> {
{
if let Ok(shadow_tree) = Ref::filter_map(self.shadow_tree.borrow(), |shadow_tree| {
shadow_tree.as_ref()
}) {
return shadow_tree;
}
}
let element = input.upcast::<Element>();
let shadow_root = element
.shadow_root()
.unwrap_or_else(|| element.attach_ua_shadow_root(cx, true));
let shadow_root = shadow_root.upcast();
*self.shadow_tree.borrow_mut() = Some(TextInputWidgetShadowTree::new(cx, shadow_root));
self.get_or_create_shadow_tree(cx, input)
}
pub(crate) fn update_shadow_tree(&self, cx: &mut JSContext, input: &HTMLInputElement) {
self.get_or_create_shadow_tree(cx, input).update(input)
}
pub(crate) fn update_placeholder_contents(&self, cx: &mut JSContext, input: &HTMLInputElement) {
self.get_or_create_shadow_tree(cx, input)
.update_placeholder(cx, input);
}
}
#[derive(Clone, JSTraceable, MallocSizeOf, PartialEq)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
/// Contains reference to text control inner editor and placeholder container element in the UA
/// shadow tree for `text`, `password`, `url`, `tel`, and `email` input. The following is the
/// structure of the shadow tree.
///
/// ```
/// <input type="text">
/// #shadow-root
/// <div id="inner-container">
/// <div id="input-editor"></div>
/// <div id="input-placeholder"></div>
/// </div>
/// </input>
/// ```
///
// TODO(stevennovaryo): We are trying to use CSS to mimic Chrome and Firefox's layout for the <input> element.
// But, this could be slower in performance and does have some discrepancies. For example,
// they would try to vertically align <input> text baseline with the baseline of other
// TextNode within an inline flow. Another example is the horizontal scroll.
// FIXME(#38263): Refactor these logics into a TextControl wrapper that would decouple all textual input.
pub(crate) struct TextInputWidgetShadowTree {
inner_container: Dom<Element>,
text_container: Dom<Element>,
placeholder_container: DomRefCell<Option<Dom<Element>>>,
}
impl TextInputWidgetShadowTree {
pub(crate) fn new(cx: &mut JSContext, shadow_root: &Node) -> Self {
let document = shadow_root.owner_document();
let inner_container = Element::create(
cx,
QualName::new(None, ns!(html), local_name!("div")),
None,
&document,
ElementCreator::ScriptCreated,
CustomElementCreationMode::Asynchronous,
None,
);
Node::replace_all(cx, Some(inner_container.upcast()), shadow_root.upcast());
inner_container
.upcast::<Node>()
.set_implemented_pseudo_element(PseudoElement::ServoTextControlInnerContainer);
let text_container = create_ua_widget_div_with_text_node(
cx,
&document,
inner_container.upcast(),
PseudoElement::ServoTextControlInnerEditor,
false,
);
Self {
inner_container: inner_container.as_traced(),
text_container: text_container.as_traced(),
placeholder_container: DomRefCell::new(None),
}
}
/// 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,
cx: &mut JSContext,
host: &HTMLInputElement,
) -> Option<DomRoot<Element>> {
if let Some(placeholder_container) = &*self.placeholder_container.borrow() {
return Some(placeholder_container.root_element());
}
// If there is no placeholder text and we haven't already created one then it is
// not necessary to initialize a new placeholder container.
if host.placeholder().is_empty() {
return None;
}
let placeholder_container = create_ua_widget_div_with_text_node(
cx,
&host.owner_document(),
self.inner_container.upcast::<Node>(),
PseudoElement::Placeholder,
true,
);
*self.placeholder_container.borrow_mut() = Some(placeholder_container.as_traced());
Some(placeholder_container)
}
fn placeholder_character_data(
&self,
cx: &mut JSContext,
input_element: &HTMLInputElement,
) -> Option<DomRoot<CharacterData>> {
self.init_placeholder_container_if_necessary(cx, input_element)
.and_then(|placeholder_container| {
let first_child = placeholder_container.upcast::<Node>().GetFirstChild()?;
Some(DomRoot::from_ref(first_child.downcast::<CharacterData>()?))
})
}
pub(crate) fn update_placeholder(&self, cx: &mut JSContext, input_element: &HTMLInputElement) {
if let Some(character_data) = self.placeholder_character_data(cx, input_element) {
let placeholder_value = input_element.placeholder().clone();
if character_data.Data() != placeholder_value {
character_data.SetData(placeholder_value);
}
}
}
fn value_character_data(&self) -> Option<DomRoot<CharacterData>> {
Some(DomRoot::from_ref(
self.text_container
.upcast::<Node>()
.GetFirstChild()?
.downcast::<CharacterData>()?,
))
}
// TODO(stevennovaryo): The rest of textual input shadow dom structure should act
// like an exstension to this one.
pub(crate) fn update(&self, input_element: &HTMLInputElement) {
// The addition of zero-width space here forces the text input to have an inline formatting
// context that might otherwise be trimmed if there's no text. This is important to ensure
// that the input element is at least as tall as the line gap of the caret:
// <https://drafts.csswg.org/css-ui/#element-with-default-preferred-size>.
//
// This is also used to ensure that the caret will still be rendered when the input is empty.
// TODO: Could append `<br>` element to prevent collapses and avoid this hack, but we would
// need to fix the rendering of caret beforehand.
let value = input_element.Value();
let input_type = &*input_element.input_type();
let value_text = match (value.is_empty(), input_type) {
// For a password input, we replace all of the character with its replacement char.
(false, InputType::Password(_)) => value
.str()
.chars()
.map(|_| PASSWORD_REPLACEMENT_CHAR)
.collect::<String>()
.into(),
(false, _) => value,
(true, _) => "\u{200B}".into(),
};
if let Some(character_data) = self.value_character_data() {
if character_data.Data() != value_text {
character_data.SetData(value_text);
}
}
}
}
/// 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(
cx: &mut JSContext,
document: &Document,
parent: &Node,
implemented_pseudo: PseudoElement,
as_first_child: bool,
) -> DomRoot<Element> {
let el = Element::create(
cx,
QualName::new(None, ns!(html), local_name!("div")),
None,
document,
ElementCreator::ScriptCreated,
CustomElementCreationMode::Asynchronous,
None,
);
parent
.upcast::<Node>()
.AppendChild(cx, el.upcast::<Node>())
.unwrap();
el.upcast::<Node>()
.set_implemented_pseudo_element(implemented_pseudo);
let text_node = document.CreateTextNode("".into(), CanGc::from_cx(cx));
if !as_first_child {
el.upcast::<Node>()
.AppendChild(cx, text_node.upcast::<Node>())
.unwrap();
} else {
el.upcast::<Node>()
.InsertBefore(
cx,
text_node.upcast::<Node>(),
el.upcast::<Node>().GetFirstChild().as_deref(),
)
.unwrap();
}
el
}