mirror of
https://github.com/servo/servo.git
synced 2025-08-03 04:30:10 +01:00
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:
parent
f523445fc3
commit
3cb16eb864
27 changed files with 710 additions and 85 deletions
|
@ -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,7 +83,18 @@ impl<'dom> NodeAndStyleInfo<'dom> {
|
|||
}
|
||||
|
||||
pub(crate) fn get_selected_style(&self) -> ServoArc<ComputedValues> {
|
||||
self.node.to_threadsafe().selected_style()
|
||||
// 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>> {
|
||||
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,27 +1212,69 @@ 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_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() {
|
||||
value.make_ascii_lowercase();
|
||||
} else {
|
||||
value = DOMString::from("#000000");
|
||||
}
|
||||
let style = format!("background-color: {value}");
|
||||
color_shadow_tree
|
||||
.color_value
|
||||
.upcast::<Element>()
|
||||
.set_string_attribute(&local_name!("style"), style.into(), can_gc);
|
||||
}
|
||||
|
||||
fn update_shadow_tree_if_needed(&self, can_gc: CanGc) {
|
||||
if 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() {
|
||||
value.make_ascii_lowercase();
|
||||
} else {
|
||||
value = DOMString::from("#000000");
|
||||
}
|
||||
let style = format!("background-color: {value}");
|
||||
color_shadow_tree
|
||||
.color_value
|
||||
.upcast::<Element>()
|
||||
.set_string_attribute(&local_name!("style"), style.into(), can_gc);
|
||||
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,22 +1583,29 @@ 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);
|
||||
{
|
||||
// Step 3.
|
||||
self.value_dirty.set(true);
|
||||
|
||||
// Step 4.
|
||||
self.sanitize_value(&mut value);
|
||||
// Step 4.
|
||||
self.sanitize_value(&mut value);
|
||||
|
||||
let mut textinput = self.textinput.borrow_mut();
|
||||
|
||||
// Step 5.
|
||||
if *textinput.single_line_content() != value {
|
||||
// Steps 1-2
|
||||
textinput.set_content(value);
|
||||
let mut textinput = self.textinput.borrow_mut();
|
||||
|
||||
// Step 5.
|
||||
textinput.clear_selection_to_limit(Direction::Forward);
|
||||
if *textinput.single_line_content() != value {
|
||||
// Steps 1-2
|
||||
textinput.set_content(value);
|
||||
|
||||
// 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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue