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

24
Cargo.lock generated
View file

@ -7249,7 +7249,7 @@ dependencies = [
[[package]]
name = "selectors"
version = "0.30.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"bitflags 2.9.1",
"cssparser",
@ -7555,7 +7555,7 @@ dependencies = [
[[package]]
name = "servo_arc"
version = "0.4.1"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"serde",
"stable_deref_trait",
@ -8017,7 +8017,7 @@ dependencies = [
[[package]]
name = "stylo"
version = "0.5.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"app_units",
"arrayvec",
@ -8074,7 +8074,7 @@ dependencies = [
[[package]]
name = "stylo_atoms"
version = "0.5.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"string_cache",
"string_cache_codegen",
@ -8083,12 +8083,12 @@ dependencies = [
[[package]]
name = "stylo_config"
version = "0.5.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
[[package]]
name = "stylo_derive"
version = "0.5.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"darling",
"proc-macro2",
@ -8100,7 +8100,7 @@ dependencies = [
[[package]]
name = "stylo_dom"
version = "0.5.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"bitflags 2.9.1",
"stylo_malloc_size_of",
@ -8109,7 +8109,7 @@ dependencies = [
[[package]]
name = "stylo_malloc_size_of"
version = "0.5.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"app_units",
"cssparser",
@ -8126,12 +8126,12 @@ dependencies = [
[[package]]
name = "stylo_static_prefs"
version = "0.5.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
[[package]]
name = "stylo_traits"
version = "0.5.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"app_units",
"bitflags 2.9.1",
@ -8548,7 +8548,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "to_shmem"
version = "0.2.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"cssparser",
"servo_arc",
@ -8561,7 +8561,7 @@ dependencies = [
[[package]]
name = "to_shmem_derive"
version = "0.1.0"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#95cc620f5a5fadf2e4bdacb17e4731c835ab381b"
source = "git+https://github.com/servo/stylo?branch=2025-07-01#14c096bd61af12a7c9bd4e48d6696cc8b16feb8b"
dependencies = [
"darling",
"proc-macro2",

View file

@ -51,8 +51,16 @@ impl<'dom> NodeAndStyleInfo<'dom> {
}
}
/// Whether this is a container for the text within a single-line text input. This
/// is used to solve the special case of line height for a text entry widget.
/// <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget>
// TODO(stevennovaryo): Remove the addition of HTMLInputElement here once all of the
// input element is implemented with UA shadow DOM. This is temporary
// workaround for past version of input element where we are
// rendering it as a bare html element.
pub(crate) fn is_single_line_text_input(&self) -> bool {
self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement)
self.node.type_id() == LayoutNodeType::Element(LayoutElementType::HTMLInputElement) ||
self.node.is_text_container_of_single_line_input()
}
pub(crate) fn pseudo(
@ -75,8 +83,19 @@ impl<'dom> NodeAndStyleInfo<'dom> {
}
pub(crate) fn get_selected_style(&self) -> ServoArc<ComputedValues> {
// This is a workaround for handling the `::selection` pseudos where it would not
// propagate to the children and Shadow DOM elements. For this case, UA widget
// inner elements should follow the originating element in terms of selection.
if self.node.is_in_ua_widget() {
self.node
.containing_shadow_host()
.expect("Ua widget inner editor is not contained")
.to_threadsafe()
.selected_style()
} else {
self.node.to_threadsafe().selected_style()
}
}
pub(crate) fn get_selection_range(&self) -> Option<Range<ByteIndex>> {
self.node.to_threadsafe().selection()
@ -194,16 +213,13 @@ fn traverse_children_of<'dom>(
) {
traverse_eager_pseudo_element(PseudoElement::Before, parent_element_info, context, handler);
// TODO(stevennovaryo): In the past we are rendering text input as a normal element,
// and the processing of text is happening here. Remove this
// special case after the implementation of UA Shadow DOM for
// all affected input elements.
if parent_element_info.node.is_text_input() {
let node_text_content = parent_element_info.node.to_threadsafe().node_text_content();
if node_text_content.is_empty() {
// 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: Is there a less hacky way to do this?
handler.handle_text(parent_element_info, "\u{200B}".into());
} else {
handler.handle_text(parent_element_info, node_text_content);

View file

@ -168,7 +168,8 @@ pub(crate) struct InlineFormattingContext {
/// Whether or not this [`InlineFormattingContext`] contains floats.
pub(super) contains_floats: bool,
/// Whether or not this is an [`InlineFormattingContext`] for a single line text input.
/// Whether or not this is an [`InlineFormattingContext`] for a single line text input's inner
/// text container.
pub(super) is_single_line_text_input: bool,
/// Whether or not this is an [`InlineFormattingContext`] has right-to-left content, which
@ -2237,8 +2238,9 @@ fn line_height(
LineHeight::Length(length) => length.0.into(),
};
// Single line text inputs line height is clamped to the size of `normal`. See
// <https://github.com/whatwg/html/pull/5462>.
// The line height of a single-line text input's inner text container is clamped to
// the size of `normal`.
// <https://html.spec.whatwg.org/multipage/#the-input-element-as-a-text-entry-widget>
if is_single_line_text_input {
line_height.max_assign(font_metrics.line_gap);
}

View file

@ -19,6 +19,36 @@ textarea {
font-size: 0.8333em;
}
input::-servo-text-control-inner-editor {
overflow-wrap: normal;
pointer-events: auto;
}
input::-servo-text-control-inner-container {
position: relative;
height: 100%;
pointer-events: none;
display: flex;
}
input:not(:placeholder-shown)::placeholder {
visibility: hidden !important
}
input::-servo-text-control-inner-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 !important;
pointer-events: none !important;
}
input::color-swatch {
width: 100%;
height: 100%;

View file

@ -1556,11 +1556,14 @@ impl Document {
return;
}
// For a node within a text input UA shadow DOM, delegate the focus target into its shadow host.
// TODO: This focus delegation should be done with shadow DOM delegateFocus attribute.
let target_el = el.find_focusable_shadow_host_if_necessary();
self.begin_focus_transaction();
// Try to focus `el`. If it's not focusable, focus the document
// instead.
// Try to focus `el`. If it's not focusable, focus the document instead.
self.request_focus(None, FocusInitiator::Local, can_gc);
self.request_focus(Some(&*el), FocusInitiator::Local, can_gc);
self.request_focus(target_el.as_deref(), FocusInitiator::Local, can_gc);
}
let dom_event = DomRoot::upcast::<Event>(MouseEvent::for_platform_mouse_event(

View file

@ -1764,6 +1764,29 @@ impl Element {
)
}
/// Returns the focusable shadow host if this is a text control inner editor.
/// This is a workaround for the focus delegation of shadow DOM and should be
/// used only to delegate focusable inner editor of [HTMLInputElement] and
/// [HTMLTextAreaElement].
pub(crate) fn find_focusable_shadow_host_if_necessary(&self) -> Option<DomRoot<Element>> {
if self.is_focusable_area() {
Some(DomRoot::from_ref(self))
} else if self.upcast::<Node>().implemented_pseudo_element() ==
Some(PseudoElement::ServoTextControlInnerEditor)
{
let containing_shadow_host = self.containing_shadow_root().map(|root| root.Host());
debug_assert!(
containing_shadow_host
.as_ref()
.is_some_and(|e| e.is_focusable_area()),
"Containing shadow host is not focusable"
);
containing_shadow_host
} else {
None
}
}
pub(crate) fn is_actually_disabled(&self) -> bool {
let node = self.upcast::<Node>();
match node.type_id() {

View file

@ -29,6 +29,8 @@ use js::rust::{HandleObject, MutableHandleObject};
use net_traits::blob_url_store::get_blob_origin;
use net_traits::filemanager_thread::{FileManagerResult, FileManagerThreadMsg};
use net_traits::{CoreResourceMsg, IpcSend};
use script_bindings::codegen::GenericBindings::CharacterDataBinding::CharacterDataMethods;
use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
use style::attr::AttrValue;
use style::selector_parser::PseudoElement;
use style::str::{split_commas, str_join};
@ -79,6 +81,7 @@ use crate::dom::node::{
use crate::dom::nodelist::NodeList;
use crate::dom::shadowroot::ShadowRoot;
use crate::dom::textcontrol::{TextControlElement, TextControlSelection};
use crate::dom::types::CharacterData;
use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor};
use crate::dom::validitystate::{ValidationFlags, ValidityState};
use crate::dom::virtualmethods::VirtualMethods;
@ -98,6 +101,31 @@ const DEFAULT_RESET_VALUE: &str = "Reset";
const PASSWORD_REPLACEMENT_CHAR: char = '●';
const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen";
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[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 `<input type=text>`. 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(stevennovaryo): Implement lazily initiated placeholder.
// FIXME(stevennovaryo): Refactor these logic into a TextControl wrapper that would handle all textual input.
struct InputTypeTextShadowTree {
text_container: Dom<HTMLDivElement>,
placeholder_container: Dom<HTMLDivElement>,
}
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
/// Contains references to the elements in the shadow tree for `<input type=range>`.
@ -112,12 +140,13 @@ struct InputTypeColorShadowTree {
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
#[non_exhaustive]
enum ShadowTree {
Text(InputTypeTextShadowTree),
Color(InputTypeColorShadowTree),
// TODO: Add shadow trees for other input types (range etc) here
}
/// <https://html.spec.whatwg.org/multipage/#attr-input-type>
#[derive(Clone, Copy, Default, JSTraceable, PartialEq)]
#[derive(Clone, Copy, Debug, Default, JSTraceable, PartialEq)]
#[allow(dead_code)]
#[derive(MallocSizeOf)]
pub(crate) enum InputType {
@ -1050,12 +1079,98 @@ impl HTMLInputElement {
/// Return a reference to the ShadowRoot that this element is a host of,
/// or create one if none exists.
// FIXME(stevennovaryo): We should encapsulate the logics for the initiation and maintainance of
// form UA widget inside another struct.
fn shadow_root(&self, can_gc: CanGc) -> DomRoot<ShadowRoot> {
self.upcast::<Element>()
.shadow_root()
.unwrap_or_else(|| self.upcast::<Element>().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<HTMLDivElement> {
let el = HTMLDivElement::new(local_name!("div"), None, document, None, can_gc);
parent
.upcast::<Node>()
.AppendChild(el.upcast::<Node>(), can_gc)
.unwrap();
el.upcast::<Node>()
.set_implemented_pseudo_element(implemented_pseudo);
let text_node = document.CreateTextNode("".into(), can_gc);
el.upcast::<Node>()
.AppendChild(text_node.upcast::<Node>(), 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);
Node::replace_all(None, shadow_root.upcast::<Node>(), can_gc);
let inner_container =
HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc);
shadow_root
.upcast::<Node>()
.AppendChild(inner_container.upcast::<Node>(), can_gc)
.unwrap();
inner_container
.upcast::<Node>()
.set_implemented_pseudo_element(PseudoElement::ServoTextControlInnerContainer);
let placeholder_container = self.create_ua_widget_div_with_text_node(
&document,
inner_container.upcast::<Node>(),
PseudoElement::Placeholder,
can_gc,
);
let text_container = self.create_ua_widget_div_with_text_node(
&document,
inner_container.upcast::<Node>(),
PseudoElement::ServoTextControlInnerEditor,
can_gc,
);
let _ = self
.shadow_tree
.borrow_mut()
.insert(ShadowTree::Text(InputTypeTextShadowTree {
text_container: text_container.as_traced(),
placeholder_container: placeholder_container.as_traced(),
}));
}
fn text_shadow_tree(&self, can_gc: CanGc) -> Ref<InputTypeTextShadowTree> {
let has_text_shadow_tree = self
.shadow_tree
.borrow()
.as_ref()
.is_some_and(|shadow_tree| matches!(shadow_tree, ShadowTree::Text(_)));
if !has_text_shadow_tree {
self.create_text_shadow_tree(can_gc);
}
let shadow_tree = self.shadow_tree.borrow();
Ref::filter_map(shadow_tree, |shadow_tree| {
let shadow_tree = shadow_tree.as_ref()?;
match shadow_tree {
ShadowTree::Text(text_tree) => Some(text_tree),
_ => None,
}
})
.ok()
.expect("UA shadow tree was not created")
}
fn create_color_shadow_tree(&self, can_gc: CanGc) {
let document = self.owner_document();
let shadow_root = self.shadow_root(can_gc);
@ -1097,15 +1212,50 @@ impl HTMLInputElement {
let shadow_tree = self.shadow_tree.borrow();
Ref::filter_map(shadow_tree, |shadow_tree| {
let shadow_tree = shadow_tree.as_ref()?;
let ShadowTree::Color(color_tree) = shadow_tree;
Some(color_tree)
match shadow_tree {
ShadowTree::Color(color_tree) => Some(color_tree),
_ => None,
}
})
.ok()
.expect("UA shadow tree was not created")
}
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
if self.input_type() == InputType::Color {
fn update_text_shadow_tree_if_needed(&self, can_gc: CanGc) {
// Should only do this for `type=text` input.
debug_assert_eq!(self.input_type(), InputType::Text);
let text_shadow_tree = self.text_shadow_tree(can_gc);
let value = self.Value();
// 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_text = match value.is_empty() {
false => value,
true => "\u{200B}".into(),
};
// FIXME(stevennovaryo): Refactor this inside a TextControl wrapper
text_shadow_tree
.text_container
.upcast::<Node>()
.GetFirstChild()
.expect("Text container without child")
.downcast::<CharacterData>()
.expect("First child is not a CharacterData node")
.SetData(value_text);
}
fn update_color_shadow_tree_if_needed(&self, can_gc: CanGc) {
// Should only do this for `type=color` input.
debug_assert_eq!(self.input_type(), InputType::Color);
let color_shadow_tree = self.color_shadow_tree(can_gc);
let mut value = self.Value();
if value.str().is_valid_simple_color_string() {
@ -1119,6 +1269,13 @@ impl HTMLInputElement {
.upcast::<Element>()
.set_string_attribute(&local_name!("style"), style.into(), can_gc);
}
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
match self.input_type() {
InputType::Text => self.update_text_shadow_tree_if_needed(can_gc),
InputType::Color => self.update_color_shadow_tree_if_needed(can_gc),
_ => {},
}
}
}
@ -1426,6 +1583,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
fn SetValue(&self, mut value: DOMString, can_gc: CanGc) -> ErrorResult {
match self.value_mode() {
ValueMode::Value => {
{
// Step 3.
self.value_dirty.set(true);
@ -1442,6 +1600,12 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
// Step 5.
textinput.clear_selection_to_limit(Direction::Forward);
}
}
// Additionally, update the placeholder shown state in another
// scope to prevent the borrow checker issue. This is normally
// being done in the attributed mutated.
self.update_placeholder_shown_state();
},
ValueMode::Default | ValueMode::DefaultOn => {
self.upcast::<Element>()
@ -2024,6 +2188,26 @@ impl HTMLInputElement {
el.set_placeholder_shown_state(has_placeholder && !has_value);
}
// Update the placeholder text in the text shadow tree.
// To increase the performance, we would only do this when it is necessary.
fn update_text_shadow_tree_placeholder(&self, can_gc: CanGc) {
if self.input_type() != InputType::Text {
return;
}
let placeholder_text = self.placeholder.borrow().clone();
// FIXME(stevennovaryo): Refactor this inside a TextControl wrapper
self.text_shadow_tree(can_gc)
.placeholder_container
.upcast::<Node>()
.GetFirstChild()
.expect("Text container without child")
.downcast::<CharacterData>()
.expect("First child is not a CharacterData node")
.SetData(placeholder_text);
}
// https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file)
// Select files by invoking UI or by passed in argument
pub(crate) fn select_files(
@ -2657,7 +2841,10 @@ impl VirtualMethods for HTMLInputElement {
}
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?
local_name!("value") if !self.value_dirty.get() => {
let value = mutation.new_value(attr).map(|value| (**value).to_owned());
let mut value = value.map_or(DOMString::new(), DOMString::from);
@ -2707,6 +2894,7 @@ impl VirtualMethods for HTMLInputElement {
}
}
self.update_placeholder_shown_state();
self.update_text_shadow_tree_placeholder(can_gc);
},
local_name!("readonly") => {
if self.input_type().is_textual() {

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();
}

View file

@ -18,6 +18,7 @@ use layout_api::{
use net_traits::image_cache::Image;
use pixels::ImageMetadata;
use range::Range;
use selectors::Element as _;
use servo_arc::Arc;
use servo_url::ServoUrl;
use style;
@ -28,7 +29,7 @@ use style::selector_parser::PseudoElement;
use super::{
ServoLayoutDocument, ServoLayoutElement, ServoShadowRoot, ServoThreadSafeLayoutElement,
};
use crate::dom::bindings::inheritance::{CharacterDataTypeId, NodeTypeId, TextTypeId};
use crate::dom::bindings::inheritance::NodeTypeId;
use crate::dom::bindings::root::LayoutDom;
use crate::dom::element::{Element, LayoutElementHelpers};
use crate::dom::node::{LayoutNodeHelpers, Node, NodeFlags, NodeTypeIdWrapper};
@ -101,6 +102,18 @@ impl<'dom> ServoLayoutNode<'dom> {
pub fn is_text_input(&self) -> bool {
self.node.is_text_input()
}
pub fn is_text_container_of_single_line_input(&self) -> bool {
self.node.is_text_container_of_single_line_input()
}
pub fn is_in_ua_widget(&self) -> bool {
self.node.is_in_ua_widget()
}
pub fn containing_shadow_host(&self) -> Option<ServoLayoutNode> {
Some(self.as_element()?.containing_shadow_host()?.as_node())
}
}
impl style::dom::NodeInfo for ServoLayoutNode<'_> {
@ -109,8 +122,7 @@ impl style::dom::NodeInfo for ServoLayoutNode<'_> {
}
fn is_text_node(&self) -> bool {
self.script_type_id() ==
NodeTypeId::CharacterData(CharacterDataTypeId::Text(TextTypeId::Text))
self.node.is_text_node_for_layout()
}
}

View file

@ -0,0 +1,2 @@
[spelling-markers-009.html]
expected: FAIL

View file

@ -0,0 +1,2 @@
[spelling-markers-010.html]
expected: FAIL

View file

@ -91,6 +91,86 @@
}
},
"reftest": {
"appearance": {
"input-text-definite-width.html": [
"fda46f8af9c14cef3911ec809054624204848b9d",
[
"appearance/input-text-definite-width.html",
[
[
"/_mozilla/appearance/input-text-definite-width-ref.html",
"=="
]
],
{}
]
],
"input-text-empty.html": [
"bd5f5f5a21ec0ce028922a6764de41dc904a1eb1",
[
"appearance/input-text-empty.html",
[
[
"/_mozilla/appearance/input-text-empty-ref.html",
"=="
]
],
{}
]
],
"input-text-nonempty-placeholder.html": [
"e075663cb6ae708b313b3cd5cd69f78c51b4bc1f",
[
"appearance/input-text-nonempty-placeholder.html",
[
[
"/_mozilla/appearance/input-text-nonempty-placeholder-ref.html",
"=="
]
],
{}
]
],
"input-text-overflow.html": [
"52db07c0f0274d2b7b086d7017982145c25918da",
[
"appearance/input-text-overflow.html",
[
[
"/_mozilla/appearance/input-text-overflow-ref.html",
"=="
]
],
{}
]
],
"input-text-placeholder-overflow.html": [
"c4d77ae2a22a5b7972f2798b8ca78742b81bacc4",
[
"appearance/input-text-placeholder-overflow.html",
[
[
"/_mozilla/appearance/input-text-placeholder-overflow-ref.html",
"=="
]
],
{}
]
],
"input-text-placeholder.html": [
"d75acade78038b14529135b1d63c0ac5a168a87b",
[
"appearance/input-text-placeholder.html",
[
[
"/_mozilla/appearance/input-text-placeholder-ref.html",
"=="
]
],
{}
]
]
},
"css": {
"abs-overflow-stackingcontext.html": [
"264df01aa64e0abe9ea3a75e57452c27d53a904f",
@ -3575,19 +3655,6 @@
{}
]
],
"input_placeholder.html": [
"f74cec8d54c04755bf5277db2e127fb0a37f855e",
[
null,
[
[
"/_mozilla/css/input_placeholder_ref.html",
"=="
]
],
{}
]
],
"input_selection_a.html": [
"fbee15aed7bcbae55a2771e9af422ce105b96a60",
[
@ -8062,6 +8129,38 @@
"b485d435a63ada28eabe976b49a8a580725e7508",
[]
],
"appearance": {
"input-text-definite-width-ref.html": [
"86f7937755750261ed3b06dfe11e78a251b9d175",
[]
],
"input-text-empty-ref.html": [
"437c9988a13e094d870f67c8de0dd0becdeece76",
[]
],
"input-text-nonempty-placeholder-ref.html": [
"5415dfb2a4a88dc3bfed6ad04e23f288534351e4",
[]
],
"input-text-overflow-ref.html": [
"4cece657a2a09cfe3f1d91d49f0c9d76f5714516",
[]
],
"input-text-placeholder-overflow-ref.html": [
"0cccfff638c0d8687a3582310c73233b7d883b1a",
[]
],
"input-text-placeholder-ref.html": [
"fa5b60bdabdf2b9b818ebe66bfc7f2711173b88b",
[]
],
"supports": {
"input-text-ref.css": [
"8cf00d493138285e50aa510273abae98c099ae8b",
[]
]
}
},
"bluetooth": {
"bluetooth-helpers.js": [
"16a280cca298bcaa5796b36b48d331bfd15baae8",
@ -9195,10 +9294,6 @@
"f29e061cc35625fa039473e702f78dbc5a7d9c14",
[]
],
"input_placeholder_ref.html": [
"1caffed07f2cdd341ad26ab7489e02233a6ae26c",
[]
],
"input_selection_incremental_ref.html": [
"95b24db3c3d7d8e02a6e1a89a07c1182bcb832de",
[]

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Input type=text With a Definite Width</title>
<link rel="stylesheet" href="./supports/input-text-ref.css">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<div id="input" class="definite-width">
Foo
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Input type=text With a Definite Width</title>
<link rel="match" href="input-text-definite-width-ref.html">
<link rel="help" href="https://github.com/servo/servo/pull/37065">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<input type="text" value="Foo" style="font-size: 1em !important; width: 100px;"></input>
</div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Empty Input type=text With a Definite Width</title>
<link rel="stylesheet" href="./supports/input-text-ref.css">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<div id="input" class="definite-width">
<br>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Empty Input type=text With a Definite Width</title>
<link rel="match" href="input-text-empty-ref.html">
<link rel="help" href="https://github.com/servo/servo/pull/37065">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<input type="text" style="font-size: 1em !important; width: 100px;"></input>
</div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of a Non-empty Input type=text With a Definite Width and a Placeholder</title>
<link rel="stylesheet" href="./supports/input-text-ref.css">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<div id="input" class="definite-width">
Foo
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of a Non-empty Input type=text With a Definite Width and a Placeholder</title>
<link rel="match" href="input-text-nonempty-placeholder-ref.html">
<link rel="help" href="https://github.com/servo/servo/pull/37065">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<input type="text" value="Foo" placeholder="Bar" style="font-size: 1em !important; width: 100px;"></input>
</div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Overflowing Input type=text With a Definite Width</title>
<link rel="stylesheet" href="./supports/input-text-ref.css">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<div id="input" class="definite-width">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Overflowing Input type=text With a Definite Width</title>
<link rel="match" href="input-text-overflow-ref.html">
<link rel="help" href="https://github.com/servo/servo/pull/37065">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<input type="text" value="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." style="font-size: 1em !important; width: 100px;"></input>
</div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Input type=text With a Definite Width and an Overflowing Placeholder</title>
<link rel="stylesheet" href="./supports/input-text-ref.css">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<div id="input" class="definite-width placeholder-color">
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Input type=text With a Definite Width and an Overflowing Placeholder</title>
<link rel="match" href="input-text-placeholder-overflow-ref.html">
<link rel="help" href="https://github.com/servo/servo/pull/37065">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<input type="text" placeholder="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." style="font-size: 1em !important; width: 100px;"></input>
</div>
</body>
</html>

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Input type=text With a Definite Width and a Placeholder</title>
<link rel="stylesheet" href="./supports/input-text-ref.css">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<div id="input" class="definite-width placeholder-color">
Bar
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Appearance of an Input type=text With a Definite Width and a Placeholder</title>
<link rel="match" href="input-text-placeholder-ref.html">
<link rel="help" href="https://github.com/servo/servo/pull/37065">
</head>
<body>
Display of an input type=text should match the display generated by the CSS reference.
<div>
<input type="text" placeholder="Bar" style="font-size: 1em !important; width: 100px;"></input>
</div>
</body>
</html>

View file

@ -0,0 +1,27 @@
/* Minimal stylesheet to mimic the appearence of an input type=text specific to Servo.
* This stylesheet is expected to be modified following the development of the
* Shadow DOM input type=text in Servo.
*/
#input {
display: inline-block;
background: white;
border: solid lightgrey 1px;
font-family: sans-serif;
font-size: 1em; /* We are using 1em here to reduce the effect of inconsistencies in layout */
overflow: hidden;
white-space: nowrap;
}
/* We are using definite width for most of the test to reduce the effect if calculating inline
* size of the input element. Which, will depends on the average character width of a font.
*
* <https://html.spec.whatwg.org/#converting-a-character-width-to-pixels>
*/
.definite-width {
width: 100px;
}
.placeholder-color {
color: grey;
}

View file

@ -1,5 +0,0 @@
<!doctype html>
<meta charset="utf-8">
<title></title>
<link rel="match" href="input_placeholder_ref.html">
<input type=text placeholder="foo bar"><input type=text placeholder="foo bar">

View file

@ -1,4 +0,0 @@
<!doctype html>
<meta charset="utf-8">
<title></title>
<input type=text value="foo bar"><input type=text value="foo bar">