Files
servo/components/script/dom/html/input_element/text_input_widget.rs
Tim van der Lippe a1c8896eda script: Pass &mut JSContext to reflect_node_with_proto (#43952)
A lot (and I mean, really a lot) depends on these constructors.
Therefore, this is the one spaghetti ball that I could extract and
convert all `can_gc` to `cx`. There are some new introductions of
`temp_cx` in the callbacks of the servo parser, but we already had some
in other callbacks.

Part of #40600

Testing: It compiles

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
2026-04-05 18:07:30 +00:00

262 lines
9.9 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/. */
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::NodeBinding::NodeMethods;
use script_bindings::inheritance::Castable;
use script_bindings::root::{Dom, DomRoot};
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::node::{Node, NodeTraits};
use crate::dom::textcontrol::TextControlElement;
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,
text_control_element: &impl TextControlElement,
) -> 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 = text_control_element.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, text_control_element)
}
pub(crate) fn update_shadow_tree(&self, cx: &mut JSContext, element: &impl TextControlElement) {
self.get_or_create_shadow_tree(cx, element).update(element)
}
pub(crate) fn update_placeholder_contents(
&self,
cx: &mut JSContext,
element: &impl TextControlElement,
) {
self.get_or_create_shadow_tree(cx, element)
.update_placeholder(cx, element);
}
}
#[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,
element: &impl TextControlElement,
) -> 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.
let placeholder = element.placeholder_text();
if placeholder.is_empty() {
return None;
}
let placeholder_container = create_ua_widget_div_with_text_node(
cx,
&element.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,
element: &impl TextControlElement,
) -> Option<DomRoot<CharacterData>> {
self.init_placeholder_container_if_necessary(cx, 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, element: &impl TextControlElement) {
if let Some(character_data) = self.placeholder_character_data(cx, element) {
let placeholder_value = element.placeholder_text();
if character_data.Data() != *placeholder_value {
character_data.SetData(placeholder_value.clone());
}
}
}
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, element: &impl TextControlElement) {
// 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 = element.value_text();
let value_text = match (value.is_empty(), element.is_password_field()) {
// For a password input, we replace all of the character with its replacement char.
(false, true) => 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(cx, "".into());
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
}