Reland Input type=text Shadow DOM With Performance Improvement (#37483)

Depends on #37427.

In addition to the changes introduced by
https://github.com/servo/servo/pull/37065, there are several performance
improvements and nits as follows:
- Use the internal pseudo element for style matching, this will reduce
the performance regression by ~66%.
- Manual construction of the `Text` node inside a text container. This
is followed by the modification of the inner `Text` node instead of
using `SetTextContent` which is more expensive.
- Use `implemented_pseudo_element` instead of
`text_control_inner_editor` `NodeFlag` to handle the special cases that
these elements should follow, specifically the:
  - focus delegation workaround;
  - selections; and
  - line height resolving.
- More documentation.

Servo's side of: https://github.com/servo/stylo/pull/217

Testing: No new unexpected WPT failure, except for the one introduced by
https://github.com/servo/servo/pull/37065/.
Fixes: #36307 #37205

---------

Signed-off-by: stevennovaryo <steven.novaryo@gmail.com>
This commit is contained in:
Jo Steven Novaryo 2025-07-23 17:08:24 +08:00 committed by GitHub
parent f523445fc3
commit 3cb16eb864
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 710 additions and 85 deletions

View file

@ -1625,6 +1625,7 @@ pub(crate) trait LayoutNodeHelpers<'dom> {
fn assigned_slot_for_layout(self) -> Option<LayoutDom<'dom, HTMLSlotElement>>;
fn is_element_for_layout(&self) -> bool;
fn is_text_node_for_layout(&self) -> bool;
unsafe fn get_flag(self, flag: NodeFlags) -> bool;
unsafe fn set_flag(self, flag: NodeFlags, value: bool);
@ -1659,7 +1660,19 @@ pub(crate) trait LayoutNodeHelpers<'dom> {
unsafe fn clear_style_and_layout_data(self);
/// Whether this element is a `<input>` rendered as text or a `<textarea>`.
/// This is used for the rendering of text control element in the past
/// where the necessities is being handled within layout. With the current
/// implementation of Shadow DOM, we are able to move and expand this kind
/// of behavior in the previous pipelines (i.e. DOM, style traversal).
fn is_text_input(&self) -> bool;
/// Whether this element serve as a container of editable text for a text input
/// that is implemented as an UA widget.
fn is_single_line_text_inner_editor(&self) -> bool;
/// Whether this element serve as a container of any text inside a text input
/// that is implemented as an UA widget.
fn is_text_container_of_single_line_input(&self) -> bool;
fn text_content(self) -> Cow<'dom, str>;
fn selection(self) -> Option<Range<usize>>;
fn image_url(self) -> Option<ServoUrl>;
@ -1694,6 +1707,11 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
(*self).is::<Element>()
}
fn is_text_node_for_layout(&self) -> bool {
self.type_id_for_layout() ==
NodeTypeId::CharacterData(CharacterDataTypeId::Text(TextTypeId::Text))
}
#[inline]
#[allow(unsafe_code)]
fn parent_node_ref(self) -> Option<LayoutDom<'dom, Node>> {
@ -1843,8 +1861,8 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
{
let input = self.unsafe_get().downcast::<HTMLInputElement>().unwrap();
// FIXME: All the non-color input types currently render as text
input.input_type() != InputType::Color
// FIXME: All the non-color and non-text input types currently render as text
!matches!(input.input_type(), InputType::Color | InputType::Text)
} else {
type_id ==
NodeTypeId::Element(ElementTypeId::HTMLElement(
@ -1853,6 +1871,30 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
}
}
fn is_single_line_text_inner_editor(&self) -> bool {
matches!(
self.unsafe_get().implemented_pseudo_element(),
Some(PseudoElement::ServoTextControlInnerEditor)
)
}
fn is_text_container_of_single_line_input(&self) -> bool {
let is_single_line_text_inner_placeholder = matches!(
self.unsafe_get().implemented_pseudo_element(),
Some(PseudoElement::Placeholder)
);
// Currently `::placeholder` is only implemented for single line text input element.
debug_assert!(
!is_single_line_text_inner_placeholder ||
self.containing_shadow_root_for_layout()
.map(|root| root.get_host_for_layout())
.map(|host| host.downcast::<HTMLInputElement>())
.is_some()
);
self.is_single_line_text_inner_editor() || is_single_line_text_inner_placeholder
}
fn text_content(self) -> Cow<'dom, str> {
if let Some(text) = self.downcast::<Text>() {
return text.upcast().data_for_layout().into();
@ -1870,6 +1912,24 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
}
fn selection(self) -> Option<Range<usize>> {
// If this is a inner editor of an UA widget element, we should find
// the selection from its shadow host.
// FIXME(stevennovaryo): This should account for the multiline text input,
// but we are yet to support that input with UA widget.
if self.is_in_ua_widget() &&
self.is_text_node_for_layout() &&
self.parent_node_ref()
.is_some_and(|parent| parent.is_single_line_text_inner_editor())
{
let shadow_root = self.containing_shadow_root_for_layout();
if let Some(containing_shadow_host) = shadow_root.map(|root| root.get_host_for_layout())
{
if let Some(input) = containing_shadow_host.downcast::<HTMLInputElement>() {
return input.selection_for_layout();
}
}
}
if let Some(area) = self.downcast::<HTMLTextAreaElement>() {
return area.selection_for_layout();
}