Implement Input type=text UA Shadow DOM (#37065)

Implement Shadow Tree construction for input `type=text`, adding a text
control inner editor container and placeholder container. Subsequently,
due to the changes of the DOM tree structure, the changes will add a new
NodeFlag `IS_TEXT_CONTROL_INNER_EDITOR` to handle the following cases.
- If a mouse click button event hits a text control inner editor, it
will redirect the focus target to its shadow host.
- In text run's construction, the text control inner editor container
queries the selection from its shadow host. This is later used to
resolve caret and selection painting in the display list.

This will be the first step of fixing input `type=text` and other
single-line text input element widgets. Such as, implementing
`::placeholder` selector.



Testing: Existing WPT test and new Servo specific appearance WPT.
Fixes: #36307

---------

Signed-off-by: stevennovaryo <steven.novaryo@gmail.com>
This commit is contained in:
Steven Novaryo 2025-05-30 20:02:10 +08:00 committed by GitHub
parent 578c52fe2b
commit 5580704438
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 635 additions and 36 deletions

View file

@ -230,6 +230,10 @@ bitflags! {
/// Whether this node has a weird parser insertion mode. i.e whether setting innerHTML
/// needs extra work or not
const HAS_WEIRD_PARSER_INSERTION_MODE = 1 << 11;
/// Whether this node serves as the text container for editable content of
/// <input> or <textarea> element.
const IS_TEXT_CONTROL_INNER_EDITOR = 1 << 12;
}
}
@ -697,6 +701,16 @@ impl Node {
self.flags.get().contains(NodeFlags::IS_CONNECTED)
}
pub(crate) fn set_text_control_inner_editor(&self) {
self.set_flag(NodeFlags::IS_TEXT_CONTROL_INNER_EDITOR, true)
}
pub(crate) fn is_text_control_inner_editor(&self) -> bool {
self.flags
.get()
.contains(NodeFlags::IS_TEXT_CONTROL_INNER_EDITOR)
}
/// Returns the type ID of this node.
pub(crate) fn type_id(&self) -> NodeTypeId {
match *self.eventtarget.type_id() {
@ -1579,6 +1593,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);
@ -1614,6 +1629,9 @@ pub(crate) trait LayoutNodeHelpers<'dom> {
/// Whether this element is a `<input>` rendered as text or a `<textarea>`.
fn is_text_input(&self) -> bool;
/// Whether this element serve as a container of editable text for a text input.
fn is_text_control_inner_editor(&self) -> bool;
fn text_content(self) -> Cow<'dom, str>;
fn selection(self) -> Option<Range<usize>>;
fn image_url(self) -> Option<ServoUrl>;
@ -1646,6 +1664,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]
fn composed_parent_node_ref(self) -> Option<LayoutDom<'dom, Node>> {
let parent = self.parent_node_ref();
@ -1787,8 +1810,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(
@ -1797,6 +1820,10 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
}
}
fn is_text_control_inner_editor(&self) -> bool {
self.unsafe_get().is_text_control_inner_editor()
}
fn text_content(self) -> Cow<'dom, str> {
if let Some(text) = self.downcast::<Text>() {
return text.upcast().data_for_layout().into();
@ -1814,6 +1841,25 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> {
}
fn selection(self) -> Option<Range<usize>> {
// This node is a text node of a text control inner editor in a <input> or <textarea> element.
// So we should find those corresponding element, and get its selection.
if self.is_text_node_for_layout() &&
self.parent_node_ref()
.is_some_and(|parent| parent.is_text_control_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(area) = containing_shadow_host.downcast::<HTMLTextAreaElement>() {
return area.selection_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();
}