diff options
author | Simon Wülker <simon.wuelker@arcor.de> | 2025-05-15 19:30:38 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-15 17:30:38 +0000 |
commit | b100a98e1d7f5ccf4af54a819a55f20f83308fef (patch) | |
tree | 02352489726897c790e7d4f5ef91e26408a4ce1f /components/script/dom | |
parent | f9382fcaa00206ce1f5f99a9dc417065d980b5ee (diff) | |
download | servo-b100a98e1d7f5ccf4af54a819a55f20f83308fef.tar.gz servo-b100a98e1d7f5ccf4af54a819a55f20f83308fef.zip |
Fully support `<input type=color>` (#36992)
This change adds a shadow-tree widget for `<input type=color>` elements.
It also involves some changes to the way layout interacts with the DOM,
because currently all `input` and `textarea` elements are rendered as
plain text and their descendants are ignored. This obviously doesn't
work for `<input type={color, date, range, etc}>`.

<details><summary>HTML used for the screenshot above</summary>
```html
<input type=color>
```
</details>
Testing: I doubt that this affects WPT tests, because the appearance and
behaviour of the widget is almost entirely unspecified.
---------
Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
Diffstat (limited to 'components/script/dom')
-rw-r--r-- | components/script/dom/htmlinputelement.rs | 304 | ||||
-rw-r--r-- | components/script/dom/htmlselectelement.rs | 8 | ||||
-rw-r--r-- | components/script/dom/node.rs | 25 |
3 files changed, 282 insertions, 55 deletions
diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index aff8168b8fe..3c78ba67772 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -12,9 +12,13 @@ use std::str::FromStr; use std::{f64, ptr}; use dom_struct::dom_struct; -use embedder_traits::{FilterPattern, InputMethodType}; +use embedder_traits::{ + EmbedderMsg, FilterPattern, FormControl as EmbedderFormControl, InputMethodType, RgbColor, +}; use encoding_rs::Encoding; +use euclid::{Point2D, Rect, Size2D}; use html5ever::{LocalName, Prefix, local_name, ns}; +use ipc_channel::ipc; use js::jsapi::{ ClippedTime, DateGetMsecSinceEpoch, Handle, JS_ClearPendingException, JSObject, NewDateObject, NewUCRegExpObject, ObjectIsDate, RegExpFlag_UnicodeSets, RegExpFlags, @@ -25,7 +29,9 @@ use js::rust::{HandleObject, MutableHandleObject}; use net_traits::blob_url_store::get_blob_origin; use net_traits::filemanager_thread::FileManagerThreadMsg; use net_traits::{CoreResourceMsg, IpcSend}; -use profile_traits::ipc; +use script_bindings::codegen::GenericBindings::ShadowRootBinding::{ + ShadowRootMode, SlotAssignmentMode, +}; use style::attr::AttrValue; use style::str::{split_commas, str_join}; use stylo_atoms::Atom; @@ -33,12 +39,12 @@ use stylo_dom::ElementState; use time::{Month, OffsetDateTime, Time}; use unicode_bidi::{BidiClass, bidi_class}; use url::Url; +use webrender_api::units::DeviceIntRect; -use super::bindings::str::{FromInputValueString, ToInputValueString}; use crate::clipboard_provider::EmbedderClipboardProvider; use crate::dom::activation::Activatable; use crate::dom::attr::Attr; -use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::cell::{DomRefCell, Ref}; use crate::dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods; use crate::dom::bindings::codegen::Bindings::FileListBinding::FileListMethods; @@ -48,30 +54,33 @@ use crate::dom::bindings::codegen::Bindings::NodeBinding::{GetRootNodeOptions, N use crate::dom::bindings::error::{Error, ErrorResult}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::reflector::DomGlobal; -use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom}; -use crate::dom::bindings::str::{DOMString, USVString}; +use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom}; +use crate::dom::bindings::str::{DOMString, FromInputValueString, ToInputValueString, USVString}; use crate::dom::clipboardevent::ClipboardEvent; use crate::dom::compositionevent::CompositionEvent; use crate::dom::document::Document; -use crate::dom::element::{AttributeMutation, Element, LayoutElementHelpers}; +use crate::dom::element::{AttributeMutation, Element, ElementCreator, LayoutElementHelpers}; use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; use crate::dom::file::File; use crate::dom::filelist::{FileList, LayoutFileListHelpers}; use crate::dom::globalscope::GlobalScope; use crate::dom::htmldatalistelement::HTMLDataListElement; +use crate::dom::htmldivelement::HTMLDivElement; use crate::dom::htmlelement::HTMLElement; use crate::dom::htmlfieldsetelement::HTMLFieldSetElement; use crate::dom::htmlformelement::{ FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, ResetFrom, SubmittedFrom, }; +use crate::dom::htmlstyleelement::HTMLStyleElement; use crate::dom::keyboardevent::KeyboardEvent; use crate::dom::mouseevent::MouseEvent; use crate::dom::node::{ BindContext, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext, }; use crate::dom::nodelist::NodeList; +use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot}; use crate::dom::textcontrol::{TextControlElement, TextControlSelection}; use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor}; use crate::dom::validitystate::{ValidationFlags, ValidityState}; @@ -92,6 +101,34 @@ 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 references to the elements in the shadow tree for `<input type=range>`. +/// +/// The shadow tree consists of a single div with the currently selected color as +/// the background. +struct InputTypeColorShadowTree { + color_value: Dom<HTMLDivElement>, +} + +#[derive(Clone, JSTraceable, MallocSizeOf)] +#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)] +#[non_exhaustive] +enum ShadowTree { + Color(InputTypeColorShadowTree), + // TODO: Add shadow trees for other input types (range etc) here +} + +const COLOR_TREE_STYLE: &str = " +#color-value { + width: 100%; + height: 100%; + box-sizing: border-box; + border: 1px solid gray; + border-radius: 2px; +} +"; + /// <https://html.spec.whatwg.org/multipage/#attr-input-type> #[derive(Clone, Copy, Default, JSTraceable, PartialEq)] #[allow(dead_code)] @@ -172,8 +209,7 @@ impl InputType { fn is_textual(&self) -> bool { matches!( *self, - InputType::Color | - InputType::Date | + InputType::Date | InputType::DatetimeLocal | InputType::Email | InputType::Hidden | @@ -277,9 +313,16 @@ impl From<&Atom> for InputType { #[derive(Debug, PartialEq)] enum ValueMode { + /// <https://html.spec.whatwg.org/multipage/#dom-input-value-value> Value, + + /// <https://html.spec.whatwg.org/multipage/#dom-input-value-default> Default, + + /// <https://html.spec.whatwg.org/multipage/#dom-input-value-default-on> DefaultOn, + + /// <https://html.spec.whatwg.org/multipage/#dom-input-value-filename> Filename, } @@ -314,6 +357,7 @@ pub(crate) struct HTMLInputElement { form_owner: MutNullableDom<HTMLFormElement>, labels_node_list: MutNullableDom<NodeList>, validity_state: MutNullableDom<ValidityState>, + shadow_tree: DomRefCell<Option<ShadowTree>>, } #[derive(JSTraceable)] @@ -372,6 +416,7 @@ impl HTMLInputElement { form_owner: Default::default(), labels_node_list: MutNullableDom::new(None), validity_state: Default::default(), + shadow_tree: Default::default(), } } @@ -803,7 +848,7 @@ impl HTMLInputElement { .map(DomRoot::from_ref) } - // https://html.spec.whatwg.org/multipage/#suffering-from-being-missing + /// <https://html.spec.whatwg.org/multipage/#suffering-from-being-missing> fn suffers_from_being_missing(&self, value: &DOMString) -> bool { match self.input_type() { // https://html.spec.whatwg.org/multipage/#checkbox-state-(type%3Dcheckbox)%3Asuffering-from-being-missing @@ -958,9 +1003,9 @@ impl HTMLInputElement { failed_flags } - // https://html.spec.whatwg.org/multipage/#suffering-from-an-underflow - // https://html.spec.whatwg.org/multipage/#suffering-from-an-overflow - // https://html.spec.whatwg.org/multipage/#suffering-from-a-step-mismatch + /// * <https://html.spec.whatwg.org/multipage/#suffering-from-an-underflow> + /// * <https://html.spec.whatwg.org/multipage/#suffering-from-an-overflow> + /// * <https://html.spec.whatwg.org/multipage/#suffering-from-a-step-mismatch> fn suffers_from_range_issues(&self, value: &DOMString) -> ValidationFlags { if value.is_empty() || !self.does_value_as_number_apply() { return ValidationFlags::empty(); @@ -1014,9 +1059,109 @@ impl HTMLInputElement { failed_flags } + + /// Return a reference to the ShadowRoot that this element is a host of, + /// or create one if none exists. + fn shadow_root(&self, can_gc: CanGc) -> DomRoot<ShadowRoot> { + self.upcast::<Element>().shadow_root().unwrap_or_else(|| { + self.upcast::<Element>() + .attach_shadow( + IsUserAgentWidget::Yes, + ShadowRootMode::Closed, + false, + false, + false, + SlotAssignmentMode::Manual, + can_gc, + ) + .expect("Attaching UA shadow root failed") + }) + } + + fn create_color_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 color_value = HTMLDivElement::new(local_name!("div"), None, &document, None, can_gc); + color_value + .upcast::<Element>() + .SetId(DOMString::from("color-value"), can_gc); + shadow_root + .upcast::<Node>() + .AppendChild(color_value.upcast::<Node>(), can_gc) + .unwrap(); + + let style = HTMLStyleElement::new( + local_name!("style"), + None, + &document, + None, + ElementCreator::ScriptCreated, + can_gc, + ); + style + .upcast::<Node>() + .SetTextContent(Some(DOMString::from(COLOR_TREE_STYLE)), can_gc); + shadow_root + .upcast::<Node>() + .AppendChild(style.upcast::<Node>(), can_gc) + .unwrap(); + + let _ = self + .shadow_tree + .borrow_mut() + .insert(ShadowTree::Color(InputTypeColorShadowTree { + color_value: color_value.as_traced(), + })); + } + + /// Get a handle to the shadow tree for this input, assuming it's [InputType] is `Color`. + /// + /// If the input is not currently a shadow host, a new shadow tree will be created. + /// + /// If the input is a shadow host for a different kind of shadow tree then the old + /// tree will be removed and a new one will be created. + fn color_shadow_tree(&self, can_gc: CanGc) -> Ref<InputTypeColorShadowTree> { + let has_color_shadow_tree = self + .shadow_tree + .borrow() + .as_ref() + .is_some_and(|shadow_tree| matches!(shadow_tree, ShadowTree::Color(_))); + if !has_color_shadow_tree { + self.create_color_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()?; + let ShadowTree::Color(color_tree) = shadow_tree; + Some(color_tree) + }) + .ok() + .expect("UA shadow tree was not created") + } + + 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); + } + } } pub(crate) trait LayoutHTMLInputElementHelpers<'dom> { + /// Return a string that represents the contents of the element for layout. fn value_for_layout(self) -> Cow<'dom, str>; fn size_for_layout(self) -> u32; fn selection_for_layout(self) -> Option<Range<usize>>; @@ -1075,16 +1220,15 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem Some(filelist) => { let length = filelist.len(); if length == 0 { - return DEFAULT_FILE_INPUT_VALUE.into(); - } - if length == 1 { + DEFAULT_FILE_INPUT_VALUE.into() + } else if length == 1 { match filelist.file_for_layout(0) { - Some(file) => return file.name().to_string().into(), - None => return DEFAULT_FILE_INPUT_VALUE.into(), + Some(file) => file.name().to_string().into(), + None => DEFAULT_FILE_INPUT_VALUE.into(), } + } else { + format!("{} files", length).into() } - - format!("{} files", length).into() }, None => DEFAULT_FILE_INPUT_VALUE.into(), } @@ -1103,6 +1247,9 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem self.placeholder().into() } }, + InputType::Color => { + unreachable!("Input type color is explicitly not rendered as text"); + }, _ => { let text = self.get_raw_textinput_value(); if !text.is_empty() { @@ -1178,11 +1325,11 @@ impl TextControlElement for HTMLInputElement { InputType::Week | InputType::Time | InputType::DatetimeLocal | - InputType::Number | - InputType::Color => true, + InputType::Number => true, InputType::Button | InputType::Checkbox | + InputType::Color | InputType::File | InputType::Hidden | InputType::Image | @@ -1257,7 +1404,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement { // https://html.spec.whatwg.org/multipage/#dom-input-checked fn SetChecked(&self, checked: bool) { self.update_checked_state(checked, true); - update_related_validity_states(self, CanGc::note()) + self.value_changed(CanGc::note()); } // https://html.spec.whatwg.org/multipage/#dom-input-readonly @@ -1349,7 +1496,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement { }, } - update_related_validity_states(self, can_gc); + self.value_changed(can_gc); self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage); Ok(()) } @@ -1724,18 +1871,6 @@ fn perform_radio_group_validation(elem: &HTMLInputElement, group: Option<&Atom>, } } -fn update_related_validity_states(elem: &HTMLInputElement, can_gc: CanGc) { - match elem.input_type() { - InputType::Radio => { - perform_radio_group_validation(elem, elem.radio_group_name().as_ref(), can_gc) - }, - _ => { - elem.validity_state() - .perform_validation_and_update(ValidationFlags::all(), can_gc); - }, - } -} - // https://html.spec.whatwg.org/multipage/#radio-button-group fn in_same_group( other: &HTMLInputElement, @@ -1905,7 +2040,7 @@ impl HTMLInputElement { InputType::Radio | InputType::Checkbox => { self.update_checked_state(self.DefaultChecked(), false); self.checked_changed.set(false); - update_related_validity_states(self, can_gc); + self.value_changed(can_gc); }, InputType::Image => (), _ => (), @@ -1949,8 +2084,9 @@ impl HTMLInputElement { .collect() }); - let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone()) - .expect("Error initializing channel"); + let (chan, recv) = + profile_traits::ipc::channel(self.global().time_profiler_chan().clone()) + .expect("Error initializing channel"); let msg = FileManagerThreadMsg::SelectFiles(webview_id, filter, chan, origin, opt_test_paths); resource_threads @@ -1977,8 +2113,9 @@ impl HTMLInputElement { None => None, }; - let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone()) - .expect("Error initializing channel"); + let (chan, recv) = + profile_traits::ipc::channel(self.global().time_profiler_chan().clone()) + .expect("Error initializing channel"); let msg = FileManagerThreadMsg::SelectFile(webview_id, filter, chan, origin, opt_test_path); resource_threads @@ -2348,6 +2485,70 @@ impl HTMLInputElement { }, }) } + + fn update_related_validity_states(&self, can_gc: CanGc) { + match self.input_type() { + InputType::Radio => { + perform_radio_group_validation(self, self.radio_group_name().as_ref(), can_gc) + }, + _ => { + self.validity_state() + .perform_validation_and_update(ValidationFlags::all(), can_gc); + }, + } + } + + fn value_changed(&self, can_gc: CanGc) { + self.update_related_validity_states(can_gc); + self.update_shadow_tree_if_needed(can_gc); + } + + /// <https://html.spec.whatwg.org/multipage/#show-the-picker,-if-applicable> + fn show_the_picker_if_applicable(&self, can_gc: CanGc) { + // FIXME: Implement most of this algorithm + + // Step 2. If element is not mutable, then return. + if !self.is_mutable() { + return; + } + + // Step 6. Otherwise, the user agent should show the relevant user interface for selecting a value for element, + // in the way it normally would when the user interacts with the control. + if self.input_type() == InputType::Color { + let (ipc_sender, ipc_receiver) = + ipc::channel::<Option<RgbColor>>().expect("Failed to create IPC channel!"); + let document = self.owner_document(); + let rect = self.upcast::<Node>().bounding_content_box_or_zero(can_gc); + let rect = Rect::new( + Point2D::new(rect.origin.x.to_px(), rect.origin.y.to_px()), + Size2D::new(rect.size.width.to_px(), rect.size.height.to_px()), + ); + let current_value = self.Value(); + let current_color = RgbColor { + red: u8::from_str_radix(¤t_value[1..3], 16).unwrap(), + green: u8::from_str_radix(¤t_value[3..5], 16).unwrap(), + blue: u8::from_str_radix(¤t_value[5..7], 16).unwrap(), + }; + document.send_to_embedder(EmbedderMsg::ShowFormControl( + document.webview_id(), + DeviceIntRect::from_untyped(&rect.to_box2d()), + EmbedderFormControl::ColorPicker(current_color, ipc_sender), + )); + + let Ok(response) = ipc_receiver.recv() else { + log::error!("Failed to receive response"); + return; + }; + + if let Some(selected_color) = response { + let formatted_color = format!( + "#{:0>2x}{:0>2x}{:0>2x}", + selected_color.red, selected_color.green, selected_color.blue + ); + let _ = self.SetValue(formatted_color.into(), can_gc); + } + } + } } impl VirtualMethods for HTMLInputElement { @@ -2359,6 +2560,7 @@ impl VirtualMethods for HTMLInputElement { self.super_type() .unwrap() .attribute_mutated(attr, mutation, can_gc); + match *attr.local_name() { local_name!("disabled") => { let disabled_state = match mutation { @@ -2553,7 +2755,7 @@ impl VirtualMethods for HTMLInputElement { _ => {}, } - update_related_validity_states(self, can_gc); + self.value_changed(can_gc); } fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue { @@ -2584,7 +2786,8 @@ impl VirtualMethods for HTMLInputElement { if self.input_type() == InputType::Radio { self.radio_group_updated(self.radio_group_name().as_ref()); } - update_related_validity_states(self, can_gc); + + self.value_changed(can_gc); } fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) { @@ -2730,7 +2933,7 @@ impl VirtualMethods for HTMLInputElement { } } - update_related_validity_states(self, can_gc); + self.value_changed(can_gc); } // https://html.spec.whatwg.org/multipage/#the-input-element%3Aconcept-node-clone-ext @@ -2752,7 +2955,7 @@ impl VirtualMethods for HTMLInputElement { elem.textinput .borrow_mut() .set_content(self.textinput.borrow().get_content()); - update_related_validity_states(self, can_gc); + self.value_changed(can_gc); } } @@ -2861,7 +3064,8 @@ impl Activatable for HTMLInputElement { }, // https://html.spec.whatwg.org/multipage/#checkbox-state-(type=checkbox):input-activation-behavior // https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):input-activation-behavior - InputType::Checkbox | InputType::Radio => true, + // https://html.spec.whatwg.org/multipage/#color-state-(type=color):input-activation-behavior + InputType::Checkbox | InputType::Radio | InputType::Color => true, _ => false, } } @@ -2907,7 +3111,7 @@ impl Activatable for HTMLInputElement { }; if activation_state.is_some() { - update_related_validity_states(self, can_gc); + self.value_changed(can_gc); } activation_state @@ -2966,7 +3170,7 @@ impl Activatable for HTMLInputElement { _ => (), } - update_related_validity_states(self, can_gc); + self.value_changed(can_gc); } /// <https://html.spec.whatwg.org/multipage/#input-activation-behavior> @@ -3028,6 +3232,10 @@ impl Activatable for HTMLInputElement { }, // https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file):input-activation-behavior InputType::File => self.select_files(None, can_gc), + // https://html.spec.whatwg.org/multipage/#color-state-(type=color):input-activation-behavior + InputType::Color => { + self.show_the_picker_if_applicable(can_gc); + }, _ => (), } } diff --git a/components/script/dom/htmlselectelement.rs b/components/script/dom/htmlselectelement.rs index 56fac20e841..0f48c8e923f 100644 --- a/components/script/dom/htmlselectelement.rs +++ b/components/script/dom/htmlselectelement.rs @@ -14,7 +14,7 @@ use style::attr::AttrValue; use stylo_dom::ElementState; use embedder_traits::{SelectElementOptionOrOptgroup, SelectElementOption}; use euclid::{Size2D, Point2D, Rect}; -use embedder_traits::EmbedderMsg; +use embedder_traits::{FormControl as EmbedderFormControl, EmbedderMsg}; use crate::dom::bindings::codegen::GenericBindings::HTMLOptGroupElementBinding::HTMLOptGroupElement_Binding::HTMLOptGroupElementMethods; use crate::dom::activation::Activatable; @@ -406,12 +406,10 @@ impl HTMLSelectElement { let selected_index = self.list_of_options().position(|option| option.Selected()); let document = self.owner_document(); - document.send_to_embedder(EmbedderMsg::ShowSelectElementMenu( + document.send_to_embedder(EmbedderMsg::ShowFormControl( document.webview_id(), - options, - selected_index, DeviceIntRect::from_untyped(&rect.to_box2d()), - ipc_sender, + EmbedderFormControl::SelectElement(options, selected_index, ipc_sender), )); let Ok(response) = ipc_receiver.recv() else { diff --git a/components/script/dom/node.rs b/components/script/dom/node.rs index 5f08abce354..17235543906 100644 --- a/components/script/dom/node.rs +++ b/components/script/dom/node.rs @@ -50,7 +50,6 @@ use style::stylesheets::{Stylesheet, UrlExtraData}; use uuid::Uuid; use xml5ever::serialize as xml_serialize; -use super::globalscope::GlobalScope; use crate::conversions::Convert; use crate::document_loader::DocumentLoader; use crate::dom::attr::Attr; @@ -92,13 +91,14 @@ use crate::dom::documenttype::DocumentType; use crate::dom::element::{CustomElementCreationMode, Element, ElementCreator, SelectorWrapper}; use crate::dom::event::{Event, EventBubbles, EventCancelable}; use crate::dom::eventtarget::EventTarget; +use crate::dom::globalscope::GlobalScope; use crate::dom::htmlbodyelement::HTMLBodyElement; use crate::dom::htmlcanvaselement::{HTMLCanvasElement, LayoutHTMLCanvasElementHelpers}; use crate::dom::htmlcollection::HTMLCollection; use crate::dom::htmlelement::HTMLElement; use crate::dom::htmliframeelement::{HTMLIFrameElement, HTMLIFrameElementLayoutMethods}; use crate::dom::htmlimageelement::{HTMLImageElement, LayoutHTMLImageElementHelpers}; -use crate::dom::htmlinputelement::{HTMLInputElement, LayoutHTMLInputElementHelpers}; +use crate::dom::htmlinputelement::{HTMLInputElement, InputType, LayoutHTMLInputElementHelpers}; use crate::dom::htmllinkelement::HTMLLinkElement; use crate::dom::htmlslotelement::{HTMLSlotElement, Slottable}; use crate::dom::htmlstyleelement::HTMLStyleElement; @@ -1612,6 +1612,8 @@ pub(crate) trait LayoutNodeHelpers<'dom> { /// attempting to read or modify the opaque layout data of this node. unsafe fn clear_style_and_layout_data(self); + /// Whether this element is a `<input>` rendered as text or a `<textarea>`. + fn is_text_input(&self) -> bool; fn text_content(self) -> Cow<'dom, str>; fn selection(self) -> Option<Range<usize>>; fn image_url(self) -> Option<ServoUrl>; @@ -1776,6 +1778,25 @@ impl<'dom> LayoutNodeHelpers<'dom> for LayoutDom<'dom, Node> { self.unsafe_get().layout_data.borrow_mut_for_layout().take(); } + fn is_text_input(&self) -> bool { + let type_id = self.type_id_for_layout(); + if type_id == + NodeTypeId::Element(ElementTypeId::HTMLElement( + HTMLElementTypeId::HTMLInputElement, + )) + { + let input = self.unsafe_get().downcast::<HTMLInputElement>().unwrap(); + + // FIXME: All the non-color input types currently render as text + input.input_type() != InputType::Color + } else { + type_id == + NodeTypeId::Element(ElementTypeId::HTMLElement( + HTMLElementTypeId::HTMLTextAreaElement, + )) + } + } + fn text_content(self) -> Cow<'dom, str> { if let Some(text) = self.downcast::<Text>() { return text.upcast().data_for_layout().into(); |