mirror of
https://github.com/servo/servo.git
synced 2025-08-04 13:10:20 +01:00
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>
This commit is contained in:
parent
f9382fcaa0
commit
b100a98e1d
14 changed files with 469 additions and 100 deletions
|
@ -239,7 +239,7 @@ mod from_script {
|
||||||
Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"),
|
Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"),
|
||||||
Self::ShutdownComplete => target_variant!("ShutdownComplete"),
|
Self::ShutdownComplete => target_variant!("ShutdownComplete"),
|
||||||
Self::ShowNotification(..) => target_variant!("ShowNotification"),
|
Self::ShowNotification(..) => target_variant!("ShowNotification"),
|
||||||
Self::ShowSelectElementMenu(..) => target_variant!("ShowSelectElementMenu"),
|
Self::ShowFormControl(..) => target_variant!("ShowFormControl"),
|
||||||
Self::FinishJavaScriptEvaluation(..) => {
|
Self::FinishJavaScriptEvaluation(..) => {
|
||||||
target_variant!("FinishJavaScriptEvaluation")
|
target_variant!("FinishJavaScriptEvaluation")
|
||||||
},
|
},
|
||||||
|
|
|
@ -201,17 +201,7 @@ fn traverse_children_of<'dom>(
|
||||||
) {
|
) {
|
||||||
traverse_eager_pseudo_element(PseudoElement::Before, parent_element, context, handler);
|
traverse_eager_pseudo_element(PseudoElement::Before, parent_element, context, handler);
|
||||||
|
|
||||||
let is_text_input_element = matches!(
|
if parent_element.is_text_input() {
|
||||||
parent_element.type_id(),
|
|
||||||
LayoutNodeType::Element(LayoutElementType::HTMLInputElement)
|
|
||||||
);
|
|
||||||
|
|
||||||
let is_textarea_element = matches!(
|
|
||||||
parent_element.type_id(),
|
|
||||||
LayoutNodeType::Element(LayoutElementType::HTMLTextAreaElement)
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_text_input_element || is_textarea_element {
|
|
||||||
let info = NodeAndStyleInfo::new(
|
let info = NodeAndStyleInfo::new(
|
||||||
parent_element,
|
parent_element,
|
||||||
parent_element.style(context.shared_context()),
|
parent_element.style(context.shared_context()),
|
||||||
|
@ -229,9 +219,7 @@ fn traverse_children_of<'dom>(
|
||||||
} else {
|
} else {
|
||||||
handler.handle_text(&info, node_text_content);
|
handler.handle_text(&info, node_text_content);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
if !is_text_input_element && !is_textarea_element {
|
|
||||||
for child in iter_child_nodes(parent_element) {
|
for child in iter_child_nodes(parent_element) {
|
||||||
if child.is_text_node() {
|
if child.is_text_node() {
|
||||||
let info = NodeAndStyleInfo::new(child, child.style(context.shared_context()));
|
let info = NodeAndStyleInfo::new(child, child.style(context.shared_context()));
|
||||||
|
|
|
@ -87,6 +87,15 @@ input[type="file"] {
|
||||||
border-style: none;
|
border-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type="color"] {
|
||||||
|
padding: 6px;
|
||||||
|
width: 64px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: lightgrey;
|
||||||
|
border: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
td[align="left"] { text-align: left; }
|
td[align="left"] { text-align: left; }
|
||||||
td[align="center"] { text-align: center; }
|
td[align="center"] { text-align: center; }
|
||||||
td[align="right"] { text-align: right; }
|
td[align="right"] { text-align: right; }
|
||||||
|
|
|
@ -12,9 +12,13 @@ use std::str::FromStr;
|
||||||
use std::{f64, ptr};
|
use std::{f64, ptr};
|
||||||
|
|
||||||
use dom_struct::dom_struct;
|
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 encoding_rs::Encoding;
|
||||||
|
use euclid::{Point2D, Rect, Size2D};
|
||||||
use html5ever::{LocalName, Prefix, local_name, ns};
|
use html5ever::{LocalName, Prefix, local_name, ns};
|
||||||
|
use ipc_channel::ipc;
|
||||||
use js::jsapi::{
|
use js::jsapi::{
|
||||||
ClippedTime, DateGetMsecSinceEpoch, Handle, JS_ClearPendingException, JSObject, NewDateObject,
|
ClippedTime, DateGetMsecSinceEpoch, Handle, JS_ClearPendingException, JSObject, NewDateObject,
|
||||||
NewUCRegExpObject, ObjectIsDate, RegExpFlag_UnicodeSets, RegExpFlags,
|
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::blob_url_store::get_blob_origin;
|
||||||
use net_traits::filemanager_thread::FileManagerThreadMsg;
|
use net_traits::filemanager_thread::FileManagerThreadMsg;
|
||||||
use net_traits::{CoreResourceMsg, IpcSend};
|
use net_traits::{CoreResourceMsg, IpcSend};
|
||||||
use profile_traits::ipc;
|
use script_bindings::codegen::GenericBindings::ShadowRootBinding::{
|
||||||
|
ShadowRootMode, SlotAssignmentMode,
|
||||||
|
};
|
||||||
use style::attr::AttrValue;
|
use style::attr::AttrValue;
|
||||||
use style::str::{split_commas, str_join};
|
use style::str::{split_commas, str_join};
|
||||||
use stylo_atoms::Atom;
|
use stylo_atoms::Atom;
|
||||||
|
@ -33,12 +39,12 @@ use stylo_dom::ElementState;
|
||||||
use time::{Month, OffsetDateTime, Time};
|
use time::{Month, OffsetDateTime, Time};
|
||||||
use unicode_bidi::{BidiClass, bidi_class};
|
use unicode_bidi::{BidiClass, bidi_class};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use webrender_api::units::DeviceIntRect;
|
||||||
|
|
||||||
use super::bindings::str::{FromInputValueString, ToInputValueString};
|
|
||||||
use crate::clipboard_provider::EmbedderClipboardProvider;
|
use crate::clipboard_provider::EmbedderClipboardProvider;
|
||||||
use crate::dom::activation::Activatable;
|
use crate::dom::activation::Activatable;
|
||||||
use crate::dom::attr::Attr;
|
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::ElementBinding::ElementMethods;
|
||||||
use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods;
|
use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods;
|
||||||
use crate::dom::bindings::codegen::Bindings::FileListBinding::FileListMethods;
|
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::error::{Error, ErrorResult};
|
||||||
use crate::dom::bindings::inheritance::Castable;
|
use crate::dom::bindings::inheritance::Castable;
|
||||||
use crate::dom::bindings::reflector::DomGlobal;
|
use crate::dom::bindings::reflector::DomGlobal;
|
||||||
use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
|
use crate::dom::bindings::root::{Dom, DomRoot, LayoutDom, MutNullableDom};
|
||||||
use crate::dom::bindings::str::{DOMString, USVString};
|
use crate::dom::bindings::str::{DOMString, FromInputValueString, ToInputValueString, USVString};
|
||||||
use crate::dom::clipboardevent::ClipboardEvent;
|
use crate::dom::clipboardevent::ClipboardEvent;
|
||||||
use crate::dom::compositionevent::CompositionEvent;
|
use crate::dom::compositionevent::CompositionEvent;
|
||||||
use crate::dom::document::Document;
|
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::event::{Event, EventBubbles, EventCancelable};
|
||||||
use crate::dom::eventtarget::EventTarget;
|
use crate::dom::eventtarget::EventTarget;
|
||||||
use crate::dom::file::File;
|
use crate::dom::file::File;
|
||||||
use crate::dom::filelist::{FileList, LayoutFileListHelpers};
|
use crate::dom::filelist::{FileList, LayoutFileListHelpers};
|
||||||
use crate::dom::globalscope::GlobalScope;
|
use crate::dom::globalscope::GlobalScope;
|
||||||
use crate::dom::htmldatalistelement::HTMLDataListElement;
|
use crate::dom::htmldatalistelement::HTMLDataListElement;
|
||||||
|
use crate::dom::htmldivelement::HTMLDivElement;
|
||||||
use crate::dom::htmlelement::HTMLElement;
|
use crate::dom::htmlelement::HTMLElement;
|
||||||
use crate::dom::htmlfieldsetelement::HTMLFieldSetElement;
|
use crate::dom::htmlfieldsetelement::HTMLFieldSetElement;
|
||||||
use crate::dom::htmlformelement::{
|
use crate::dom::htmlformelement::{
|
||||||
FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, ResetFrom,
|
FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, ResetFrom,
|
||||||
SubmittedFrom,
|
SubmittedFrom,
|
||||||
};
|
};
|
||||||
|
use crate::dom::htmlstyleelement::HTMLStyleElement;
|
||||||
use crate::dom::keyboardevent::KeyboardEvent;
|
use crate::dom::keyboardevent::KeyboardEvent;
|
||||||
use crate::dom::mouseevent::MouseEvent;
|
use crate::dom::mouseevent::MouseEvent;
|
||||||
use crate::dom::node::{
|
use crate::dom::node::{
|
||||||
BindContext, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext,
|
BindContext, CloneChildrenFlag, Node, NodeDamage, NodeTraits, ShadowIncluding, UnbindContext,
|
||||||
};
|
};
|
||||||
use crate::dom::nodelist::NodeList;
|
use crate::dom::nodelist::NodeList;
|
||||||
|
use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot};
|
||||||
use crate::dom::textcontrol::{TextControlElement, TextControlSelection};
|
use crate::dom::textcontrol::{TextControlElement, TextControlSelection};
|
||||||
use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor};
|
use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor};
|
||||||
use crate::dom::validitystate::{ValidationFlags, ValidityState};
|
use crate::dom::validitystate::{ValidationFlags, ValidityState};
|
||||||
|
@ -92,6 +101,34 @@ const DEFAULT_RESET_VALUE: &str = "Reset";
|
||||||
const PASSWORD_REPLACEMENT_CHAR: char = '●';
|
const PASSWORD_REPLACEMENT_CHAR: char = '●';
|
||||||
const DEFAULT_FILE_INPUT_VALUE: &str = "No file chosen";
|
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>
|
/// <https://html.spec.whatwg.org/multipage/#attr-input-type>
|
||||||
#[derive(Clone, Copy, Default, JSTraceable, PartialEq)]
|
#[derive(Clone, Copy, Default, JSTraceable, PartialEq)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -172,8 +209,7 @@ impl InputType {
|
||||||
fn is_textual(&self) -> bool {
|
fn is_textual(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
*self,
|
*self,
|
||||||
InputType::Color |
|
InputType::Date |
|
||||||
InputType::Date |
|
|
||||||
InputType::DatetimeLocal |
|
InputType::DatetimeLocal |
|
||||||
InputType::Email |
|
InputType::Email |
|
||||||
InputType::Hidden |
|
InputType::Hidden |
|
||||||
|
@ -277,9 +313,16 @@ impl From<&Atom> for InputType {
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum ValueMode {
|
enum ValueMode {
|
||||||
|
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-value>
|
||||||
Value,
|
Value,
|
||||||
|
|
||||||
|
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-default>
|
||||||
Default,
|
Default,
|
||||||
|
|
||||||
|
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-default-on>
|
||||||
DefaultOn,
|
DefaultOn,
|
||||||
|
|
||||||
|
/// <https://html.spec.whatwg.org/multipage/#dom-input-value-filename>
|
||||||
Filename,
|
Filename,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,6 +357,7 @@ pub(crate) struct HTMLInputElement {
|
||||||
form_owner: MutNullableDom<HTMLFormElement>,
|
form_owner: MutNullableDom<HTMLFormElement>,
|
||||||
labels_node_list: MutNullableDom<NodeList>,
|
labels_node_list: MutNullableDom<NodeList>,
|
||||||
validity_state: MutNullableDom<ValidityState>,
|
validity_state: MutNullableDom<ValidityState>,
|
||||||
|
shadow_tree: DomRefCell<Option<ShadowTree>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(JSTraceable)]
|
#[derive(JSTraceable)]
|
||||||
|
@ -372,6 +416,7 @@ impl HTMLInputElement {
|
||||||
form_owner: Default::default(),
|
form_owner: Default::default(),
|
||||||
labels_node_list: MutNullableDom::new(None),
|
labels_node_list: MutNullableDom::new(None),
|
||||||
validity_state: Default::default(),
|
validity_state: Default::default(),
|
||||||
|
shadow_tree: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -803,7 +848,7 @@ impl HTMLInputElement {
|
||||||
.map(DomRoot::from_ref)
|
.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 {
|
fn suffers_from_being_missing(&self, value: &DOMString) -> bool {
|
||||||
match self.input_type() {
|
match self.input_type() {
|
||||||
// https://html.spec.whatwg.org/multipage/#checkbox-state-(type%3Dcheckbox)%3Asuffering-from-being-missing
|
// https://html.spec.whatwg.org/multipage/#checkbox-state-(type%3Dcheckbox)%3Asuffering-from-being-missing
|
||||||
|
@ -958,9 +1003,9 @@ impl HTMLInputElement {
|
||||||
failed_flags
|
failed_flags
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/multipage/#suffering-from-an-underflow
|
/// * <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-an-overflow>
|
||||||
// https://html.spec.whatwg.org/multipage/#suffering-from-a-step-mismatch
|
/// * <https://html.spec.whatwg.org/multipage/#suffering-from-a-step-mismatch>
|
||||||
fn suffers_from_range_issues(&self, value: &DOMString) -> ValidationFlags {
|
fn suffers_from_range_issues(&self, value: &DOMString) -> ValidationFlags {
|
||||||
if value.is_empty() || !self.does_value_as_number_apply() {
|
if value.is_empty() || !self.does_value_as_number_apply() {
|
||||||
return ValidationFlags::empty();
|
return ValidationFlags::empty();
|
||||||
|
@ -1014,9 +1059,109 @@ impl HTMLInputElement {
|
||||||
|
|
||||||
failed_flags
|
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> {
|
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 value_for_layout(self) -> Cow<'dom, str>;
|
||||||
fn size_for_layout(self) -> u32;
|
fn size_for_layout(self) -> u32;
|
||||||
fn selection_for_layout(self) -> Option<Range<usize>>;
|
fn selection_for_layout(self) -> Option<Range<usize>>;
|
||||||
|
@ -1075,16 +1220,15 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
|
||||||
Some(filelist) => {
|
Some(filelist) => {
|
||||||
let length = filelist.len();
|
let length = filelist.len();
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return DEFAULT_FILE_INPUT_VALUE.into();
|
DEFAULT_FILE_INPUT_VALUE.into()
|
||||||
}
|
} else if length == 1 {
|
||||||
if length == 1 {
|
|
||||||
match filelist.file_for_layout(0) {
|
match filelist.file_for_layout(0) {
|
||||||
Some(file) => return file.name().to_string().into(),
|
Some(file) => file.name().to_string().into(),
|
||||||
None => return DEFAULT_FILE_INPUT_VALUE.into(),
|
None => DEFAULT_FILE_INPUT_VALUE.into(),
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
format!("{} files", length).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
format!("{} files", length).into()
|
|
||||||
},
|
},
|
||||||
None => DEFAULT_FILE_INPUT_VALUE.into(),
|
None => DEFAULT_FILE_INPUT_VALUE.into(),
|
||||||
}
|
}
|
||||||
|
@ -1103,6 +1247,9 @@ impl<'dom> LayoutHTMLInputElementHelpers<'dom> for LayoutDom<'dom, HTMLInputElem
|
||||||
self.placeholder().into()
|
self.placeholder().into()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
InputType::Color => {
|
||||||
|
unreachable!("Input type color is explicitly not rendered as text");
|
||||||
|
},
|
||||||
_ => {
|
_ => {
|
||||||
let text = self.get_raw_textinput_value();
|
let text = self.get_raw_textinput_value();
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
|
@ -1178,11 +1325,11 @@ impl TextControlElement for HTMLInputElement {
|
||||||
InputType::Week |
|
InputType::Week |
|
||||||
InputType::Time |
|
InputType::Time |
|
||||||
InputType::DatetimeLocal |
|
InputType::DatetimeLocal |
|
||||||
InputType::Number |
|
InputType::Number => true,
|
||||||
InputType::Color => true,
|
|
||||||
|
|
||||||
InputType::Button |
|
InputType::Button |
|
||||||
InputType::Checkbox |
|
InputType::Checkbox |
|
||||||
|
InputType::Color |
|
||||||
InputType::File |
|
InputType::File |
|
||||||
InputType::Hidden |
|
InputType::Hidden |
|
||||||
InputType::Image |
|
InputType::Image |
|
||||||
|
@ -1257,7 +1404,7 @@ impl HTMLInputElementMethods<crate::DomTypeHolder> for HTMLInputElement {
|
||||||
// https://html.spec.whatwg.org/multipage/#dom-input-checked
|
// https://html.spec.whatwg.org/multipage/#dom-input-checked
|
||||||
fn SetChecked(&self, checked: bool) {
|
fn SetChecked(&self, checked: bool) {
|
||||||
self.update_checked_state(checked, true);
|
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
|
// 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);
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||||
Ok(())
|
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
|
// https://html.spec.whatwg.org/multipage/#radio-button-group
|
||||||
fn in_same_group(
|
fn in_same_group(
|
||||||
other: &HTMLInputElement,
|
other: &HTMLInputElement,
|
||||||
|
@ -1905,7 +2040,7 @@ impl HTMLInputElement {
|
||||||
InputType::Radio | InputType::Checkbox => {
|
InputType::Radio | InputType::Checkbox => {
|
||||||
self.update_checked_state(self.DefaultChecked(), false);
|
self.update_checked_state(self.DefaultChecked(), false);
|
||||||
self.checked_changed.set(false);
|
self.checked_changed.set(false);
|
||||||
update_related_validity_states(self, can_gc);
|
self.value_changed(can_gc);
|
||||||
},
|
},
|
||||||
InputType::Image => (),
|
InputType::Image => (),
|
||||||
_ => (),
|
_ => (),
|
||||||
|
@ -1949,8 +2084,9 @@ impl HTMLInputElement {
|
||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone())
|
let (chan, recv) =
|
||||||
.expect("Error initializing channel");
|
profile_traits::ipc::channel(self.global().time_profiler_chan().clone())
|
||||||
|
.expect("Error initializing channel");
|
||||||
let msg =
|
let msg =
|
||||||
FileManagerThreadMsg::SelectFiles(webview_id, filter, chan, origin, opt_test_paths);
|
FileManagerThreadMsg::SelectFiles(webview_id, filter, chan, origin, opt_test_paths);
|
||||||
resource_threads
|
resource_threads
|
||||||
|
@ -1977,8 +2113,9 @@ impl HTMLInputElement {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (chan, recv) = ipc::channel(self.global().time_profiler_chan().clone())
|
let (chan, recv) =
|
||||||
.expect("Error initializing channel");
|
profile_traits::ipc::channel(self.global().time_profiler_chan().clone())
|
||||||
|
.expect("Error initializing channel");
|
||||||
let msg =
|
let msg =
|
||||||
FileManagerThreadMsg::SelectFile(webview_id, filter, chan, origin, opt_test_path);
|
FileManagerThreadMsg::SelectFile(webview_id, filter, chan, origin, opt_test_path);
|
||||||
resource_threads
|
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 {
|
impl VirtualMethods for HTMLInputElement {
|
||||||
|
@ -2359,6 +2560,7 @@ impl VirtualMethods for HTMLInputElement {
|
||||||
self.super_type()
|
self.super_type()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.attribute_mutated(attr, mutation, can_gc);
|
.attribute_mutated(attr, mutation, can_gc);
|
||||||
|
|
||||||
match *attr.local_name() {
|
match *attr.local_name() {
|
||||||
local_name!("disabled") => {
|
local_name!("disabled") => {
|
||||||
let disabled_state = match mutation {
|
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 {
|
fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
|
||||||
|
@ -2584,7 +2786,8 @@ impl VirtualMethods for HTMLInputElement {
|
||||||
if self.input_type() == InputType::Radio {
|
if self.input_type() == InputType::Radio {
|
||||||
self.radio_group_updated(self.radio_group_name().as_ref());
|
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) {
|
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
|
// https://html.spec.whatwg.org/multipage/#the-input-element%3Aconcept-node-clone-ext
|
||||||
|
@ -2752,7 +2955,7 @@ impl VirtualMethods for HTMLInputElement {
|
||||||
elem.textinput
|
elem.textinput
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.set_content(self.textinput.borrow().get_content());
|
.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/#checkbox-state-(type=checkbox):input-activation-behavior
|
||||||
// https://html.spec.whatwg.org/multipage/#radio-button-state-(type=radio):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,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2907,7 +3111,7 @@ impl Activatable for HTMLInputElement {
|
||||||
};
|
};
|
||||||
|
|
||||||
if activation_state.is_some() {
|
if activation_state.is_some() {
|
||||||
update_related_validity_states(self, can_gc);
|
self.value_changed(can_gc);
|
||||||
}
|
}
|
||||||
|
|
||||||
activation_state
|
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>
|
/// <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
|
// https://html.spec.whatwg.org/multipage/#file-upload-state-(type=file):input-activation-behavior
|
||||||
InputType::File => self.select_files(None, can_gc),
|
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);
|
||||||
|
},
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use style::attr::AttrValue;
|
||||||
use stylo_dom::ElementState;
|
use stylo_dom::ElementState;
|
||||||
use embedder_traits::{SelectElementOptionOrOptgroup, SelectElementOption};
|
use embedder_traits::{SelectElementOptionOrOptgroup, SelectElementOption};
|
||||||
use euclid::{Size2D, Point2D, Rect};
|
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::bindings::codegen::GenericBindings::HTMLOptGroupElementBinding::HTMLOptGroupElement_Binding::HTMLOptGroupElementMethods;
|
||||||
use crate::dom::activation::Activatable;
|
use crate::dom::activation::Activatable;
|
||||||
|
@ -406,12 +406,10 @@ impl HTMLSelectElement {
|
||||||
let selected_index = self.list_of_options().position(|option| option.Selected());
|
let selected_index = self.list_of_options().position(|option| option.Selected());
|
||||||
|
|
||||||
let document = self.owner_document();
|
let document = self.owner_document();
|
||||||
document.send_to_embedder(EmbedderMsg::ShowSelectElementMenu(
|
document.send_to_embedder(EmbedderMsg::ShowFormControl(
|
||||||
document.webview_id(),
|
document.webview_id(),
|
||||||
options,
|
|
||||||
selected_index,
|
|
||||||
DeviceIntRect::from_untyped(&rect.to_box2d()),
|
DeviceIntRect::from_untyped(&rect.to_box2d()),
|
||||||
ipc_sender,
|
EmbedderFormControl::SelectElement(options, selected_index, ipc_sender),
|
||||||
));
|
));
|
||||||
|
|
||||||
let Ok(response) = ipc_receiver.recv() else {
|
let Ok(response) = ipc_receiver.recv() else {
|
||||||
|
|
|
@ -50,7 +50,6 @@ use style::stylesheets::{Stylesheet, UrlExtraData};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use xml5ever::serialize as xml_serialize;
|
use xml5ever::serialize as xml_serialize;
|
||||||
|
|
||||||
use super::globalscope::GlobalScope;
|
|
||||||
use crate::conversions::Convert;
|
use crate::conversions::Convert;
|
||||||
use crate::document_loader::DocumentLoader;
|
use crate::document_loader::DocumentLoader;
|
||||||
use crate::dom::attr::Attr;
|
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::element::{CustomElementCreationMode, Element, ElementCreator, SelectorWrapper};
|
||||||
use crate::dom::event::{Event, EventBubbles, EventCancelable};
|
use crate::dom::event::{Event, EventBubbles, EventCancelable};
|
||||||
use crate::dom::eventtarget::EventTarget;
|
use crate::dom::eventtarget::EventTarget;
|
||||||
|
use crate::dom::globalscope::GlobalScope;
|
||||||
use crate::dom::htmlbodyelement::HTMLBodyElement;
|
use crate::dom::htmlbodyelement::HTMLBodyElement;
|
||||||
use crate::dom::htmlcanvaselement::{HTMLCanvasElement, LayoutHTMLCanvasElementHelpers};
|
use crate::dom::htmlcanvaselement::{HTMLCanvasElement, LayoutHTMLCanvasElementHelpers};
|
||||||
use crate::dom::htmlcollection::HTMLCollection;
|
use crate::dom::htmlcollection::HTMLCollection;
|
||||||
use crate::dom::htmlelement::HTMLElement;
|
use crate::dom::htmlelement::HTMLElement;
|
||||||
use crate::dom::htmliframeelement::{HTMLIFrameElement, HTMLIFrameElementLayoutMethods};
|
use crate::dom::htmliframeelement::{HTMLIFrameElement, HTMLIFrameElementLayoutMethods};
|
||||||
use crate::dom::htmlimageelement::{HTMLImageElement, LayoutHTMLImageElementHelpers};
|
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::htmllinkelement::HTMLLinkElement;
|
||||||
use crate::dom::htmlslotelement::{HTMLSlotElement, Slottable};
|
use crate::dom::htmlslotelement::{HTMLSlotElement, Slottable};
|
||||||
use crate::dom::htmlstyleelement::HTMLStyleElement;
|
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.
|
/// attempting to read or modify the opaque layout data of this node.
|
||||||
unsafe fn clear_style_and_layout_data(self);
|
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 text_content(self) -> Cow<'dom, str>;
|
||||||
fn selection(self) -> Option<Range<usize>>;
|
fn selection(self) -> Option<Range<usize>>;
|
||||||
fn image_url(self) -> Option<ServoUrl>;
|
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();
|
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> {
|
fn text_content(self) -> Cow<'dom, str> {
|
||||||
if let Some(text) = self.downcast::<Text>() {
|
if let Some(text) = self.downcast::<Text>() {
|
||||||
return text.upcast().data_for_layout().into();
|
return text.upcast().data_for_layout().into();
|
||||||
|
|
|
@ -96,6 +96,10 @@ impl<'dom> ServoLayoutNode<'dom> {
|
||||||
.map(LayoutDom::upcast)
|
.map(LayoutDom::upcast)
|
||||||
.map(ServoLayoutElement::from_layout_js)
|
.map(ServoLayoutElement::from_layout_js)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_text_input(&self) -> bool {
|
||||||
|
self.node.is_text_input()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl style::dom::NodeInfo for ServoLayoutNode<'_> {
|
impl style::dom::NodeInfo for ServoLayoutNode<'_> {
|
||||||
|
|
|
@ -66,6 +66,7 @@ use constellation::{
|
||||||
};
|
};
|
||||||
use constellation_traits::{EmbedderToConstellationMessage, ScriptToConstellationChan};
|
use constellation_traits::{EmbedderToConstellationMessage, ScriptToConstellationChan};
|
||||||
use crossbeam_channel::{Receiver, Sender, unbounded};
|
use crossbeam_channel::{Receiver, Sender, unbounded};
|
||||||
|
use embedder_traits::FormControl as EmbedderFormControl;
|
||||||
use embedder_traits::user_content_manager::UserContentManager;
|
use embedder_traits::user_content_manager::UserContentManager;
|
||||||
pub use embedder_traits::*;
|
pub use embedder_traits::*;
|
||||||
use env_logger::Builder as EnvLoggerBuilder;
|
use env_logger::Builder as EnvLoggerBuilder;
|
||||||
|
@ -125,8 +126,8 @@ pub use crate::servo_delegate::{ServoDelegate, ServoError};
|
||||||
use crate::webrender_api::FrameReadyParams;
|
use crate::webrender_api::FrameReadyParams;
|
||||||
pub use crate::webview::{WebView, WebViewBuilder};
|
pub use crate::webview::{WebView, WebViewBuilder};
|
||||||
pub use crate::webview_delegate::{
|
pub use crate::webview_delegate::{
|
||||||
AllowOrDenyRequest, AuthenticationRequest, FormControl, NavigationRequest, PermissionRequest,
|
AllowOrDenyRequest, AuthenticationRequest, ColorPicker, FormControl, NavigationRequest,
|
||||||
SelectElement, WebResourceLoad, WebViewDelegate,
|
PermissionRequest, SelectElement, WebResourceLoad, WebViewDelegate,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "webdriver")]
|
#[cfg(feature = "webdriver")]
|
||||||
|
@ -965,18 +966,30 @@ impl Servo {
|
||||||
None => self.delegate().show_notification(notification),
|
None => self.delegate().show_notification(notification),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EmbedderMsg::ShowSelectElementMenu(
|
EmbedderMsg::ShowFormControl(webview_id, position, form_control) => {
|
||||||
webview_id,
|
|
||||||
options,
|
|
||||||
selected_option,
|
|
||||||
position,
|
|
||||||
ipc_sender,
|
|
||||||
) => {
|
|
||||||
if let Some(webview) = self.get_webview_handle(webview_id) {
|
if let Some(webview) = self.get_webview_handle(webview_id) {
|
||||||
let prompt = SelectElement::new(options, selected_option, position, ipc_sender);
|
let form_control = match form_control {
|
||||||
webview
|
EmbedderFormControl::SelectElement(
|
||||||
.delegate()
|
options,
|
||||||
.show_form_control(webview, FormControl::SelectElement(prompt));
|
selected_option,
|
||||||
|
ipc_sender,
|
||||||
|
) => FormControl::SelectElement(SelectElement::new(
|
||||||
|
options,
|
||||||
|
selected_option,
|
||||||
|
position,
|
||||||
|
ipc_sender,
|
||||||
|
)),
|
||||||
|
EmbedderFormControl::ColorPicker(current_color, ipc_sender) => {
|
||||||
|
FormControl::ColorPicker(ColorPicker::new(
|
||||||
|
current_color,
|
||||||
|
position,
|
||||||
|
ipc_sender,
|
||||||
|
self.servo_errors.sender(),
|
||||||
|
))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
webview.delegate().show_form_control(webview, form_control);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ use constellation_traits::EmbedderToConstellationMessage;
|
||||||
use embedder_traits::{
|
use embedder_traits::{
|
||||||
AllowOrDeny, AuthenticationResponse, ContextMenuResult, Cursor, FilterPattern,
|
AllowOrDeny, AuthenticationResponse, ContextMenuResult, Cursor, FilterPattern,
|
||||||
GamepadHapticEffectType, InputMethodType, LoadStatus, MediaSessionEvent, Notification,
|
GamepadHapticEffectType, InputMethodType, LoadStatus, MediaSessionEvent, Notification,
|
||||||
PermissionFeature, ScreenGeometry, SelectElementOptionOrOptgroup, SimpleDialog,
|
PermissionFeature, RgbColor, ScreenGeometry, SelectElementOptionOrOptgroup, SimpleDialog,
|
||||||
WebResourceRequest, WebResourceResponse, WebResourceResponseMsg,
|
WebResourceRequest, WebResourceResponse, WebResourceResponseMsg,
|
||||||
};
|
};
|
||||||
use ipc_channel::ipc::IpcSender;
|
use ipc_channel::ipc::IpcSender;
|
||||||
|
@ -300,6 +300,8 @@ impl Drop for InterceptedWebResourceLoad {
|
||||||
pub enum FormControl {
|
pub enum FormControl {
|
||||||
/// The picker of a `<select>` element.
|
/// The picker of a `<select>` element.
|
||||||
SelectElement(SelectElement),
|
SelectElement(SelectElement),
|
||||||
|
/// The picker of a `<input type=color>` element.
|
||||||
|
ColorPicker(ColorPicker),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a dialog triggered by clicking a `<select>` element.
|
/// Represents a dialog triggered by clicking a `<select>` element.
|
||||||
|
@ -356,6 +358,48 @@ impl SelectElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a dialog triggered by clicking a `<input type=color>` element.
|
||||||
|
pub struct ColorPicker {
|
||||||
|
pub(crate) current_color: RgbColor,
|
||||||
|
pub(crate) position: DeviceIntRect,
|
||||||
|
pub(crate) responder: IpcResponder<Option<RgbColor>>,
|
||||||
|
pub(crate) error_sender: ServoErrorSender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorPicker {
|
||||||
|
pub(crate) fn new(
|
||||||
|
current_color: RgbColor,
|
||||||
|
position: DeviceIntRect,
|
||||||
|
ipc_sender: IpcSender<Option<RgbColor>>,
|
||||||
|
error_sender: ServoErrorSender,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
current_color,
|
||||||
|
position,
|
||||||
|
responder: IpcResponder::new(ipc_sender, None),
|
||||||
|
error_sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the area occupied by the `<input>` element that triggered the prompt.
|
||||||
|
///
|
||||||
|
/// The embedder should use this value to position the prompt that is shown to the user.
|
||||||
|
pub fn position(&self) -> DeviceIntRect {
|
||||||
|
self.position
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the color that was selected before the prompt was opened.
|
||||||
|
pub fn current_color(&self) -> RgbColor {
|
||||||
|
self.current_color
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select(&mut self, color: Option<RgbColor>) {
|
||||||
|
if let Err(error) = self.responder.send(color) {
|
||||||
|
self.error_sender.raise_response_send_error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait WebViewDelegate {
|
pub trait WebViewDelegate {
|
||||||
/// Get the [`ScreenGeometry`] for this [`WebView`]. If this is unimplemented or returns `None`
|
/// Get the [`ScreenGeometry`] for this [`WebView`]. If this is unimplemented or returns `None`
|
||||||
/// the screen will have the size of the [`WebView`]'s `RenderingContext` and `WebView` will be
|
/// the screen will have the size of the [`WebView`]'s `RenderingContext` and `WebView` will be
|
||||||
|
|
|
@ -364,16 +364,8 @@ pub enum EmbedderMsg {
|
||||||
ShutdownComplete,
|
ShutdownComplete,
|
||||||
/// Request to display a notification.
|
/// Request to display a notification.
|
||||||
ShowNotification(Option<WebViewId>, Notification),
|
ShowNotification(Option<WebViewId>, Notification),
|
||||||
/// Indicates that the user has activated a `<select>` element.
|
/// Request to display a form control to the embedder.
|
||||||
///
|
ShowFormControl(WebViewId, DeviceIntRect, FormControl),
|
||||||
/// The embedder should respond with the new state of the `<select>` element.
|
|
||||||
ShowSelectElementMenu(
|
|
||||||
WebViewId,
|
|
||||||
Vec<SelectElementOptionOrOptgroup>,
|
|
||||||
Option<usize>,
|
|
||||||
DeviceIntRect,
|
|
||||||
IpcSender<Option<usize>>,
|
|
||||||
),
|
|
||||||
/// Inform the embedding layer that a JavaScript evaluation has
|
/// Inform the embedding layer that a JavaScript evaluation has
|
||||||
/// finished with the given result.
|
/// finished with the given result.
|
||||||
FinishJavaScriptEvaluation(
|
FinishJavaScriptEvaluation(
|
||||||
|
@ -389,6 +381,18 @@ impl Debug for EmbedderMsg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
pub enum FormControl {
|
||||||
|
/// Indicates that the user has activated a `<select>` element.
|
||||||
|
SelectElement(
|
||||||
|
Vec<SelectElementOptionOrOptgroup>,
|
||||||
|
Option<usize>,
|
||||||
|
IpcSender<Option<usize>>,
|
||||||
|
),
|
||||||
|
/// Indicates that the user has activated a `<input type=color>` element.
|
||||||
|
ColorPicker(RgbColor, IpcSender<Option<RgbColor>>),
|
||||||
|
}
|
||||||
|
|
||||||
/// Filter for file selection;
|
/// Filter for file selection;
|
||||||
/// the `String` content is expected to be extension (e.g, "doc", without the prefixing ".")
|
/// the `String` content is expected to be extension (e.g, "doc", without the prefixing ".")
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
@ -921,3 +925,10 @@ pub enum JavaScriptEvaluationError {
|
||||||
/// value into a [`JSValue`].
|
/// value into a [`JSValue`].
|
||||||
SerializationError,
|
SerializationError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct RgbColor {
|
||||||
|
pub red: u8,
|
||||||
|
pub green: u8,
|
||||||
|
pub blue: u8,
|
||||||
|
}
|
||||||
|
|
|
@ -609,6 +609,15 @@ impl WebViewDelegate for RunningAppState {
|
||||||
let offset = self.inner().window.toolbar_height();
|
let offset = self.inner().window.toolbar_height();
|
||||||
self.add_dialog(webview, Dialog::new_select_element_dialog(prompt, offset));
|
self.add_dialog(webview, Dialog::new_select_element_dialog(prompt, offset));
|
||||||
},
|
},
|
||||||
|
FormControl::ColorPicker(color_picker) => {
|
||||||
|
// FIXME: Reading the toolbar height is needed here to properly position the select dialog.
|
||||||
|
// But if the toolbar height changes while the dialog is open then the position won't be updated
|
||||||
|
let offset = self.inner().window.toolbar_height();
|
||||||
|
self.add_dialog(
|
||||||
|
webview,
|
||||||
|
Dialog::new_color_picker_dialog(color_picker, offset),
|
||||||
|
);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,9 @@ use log::warn;
|
||||||
use servo::ipc_channel::ipc::IpcSender;
|
use servo::ipc_channel::ipc::IpcSender;
|
||||||
use servo::servo_geometry::DeviceIndependentPixel;
|
use servo::servo_geometry::DeviceIndependentPixel;
|
||||||
use servo::{
|
use servo::{
|
||||||
AlertResponse, AuthenticationRequest, ConfirmResponse, FilterPattern, PermissionRequest,
|
AlertResponse, AuthenticationRequest, ColorPicker, ConfirmResponse, FilterPattern,
|
||||||
PromptResponse, SelectElement, SelectElementOption, SelectElementOptionOrOptgroup,
|
PermissionRequest, PromptResponse, RgbColor, SelectElement, SelectElementOption,
|
||||||
SimpleDialog,
|
SelectElementOptionOrOptgroup, SimpleDialog,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub enum Dialog {
|
pub enum Dialog {
|
||||||
|
@ -43,6 +43,11 @@ pub enum Dialog {
|
||||||
maybe_prompt: Option<SelectElement>,
|
maybe_prompt: Option<SelectElement>,
|
||||||
toolbar_offset: Length<f32, DeviceIndependentPixel>,
|
toolbar_offset: Length<f32, DeviceIndependentPixel>,
|
||||||
},
|
},
|
||||||
|
ColorPicker {
|
||||||
|
current_color: egui::Color32,
|
||||||
|
maybe_prompt: Option<ColorPicker>,
|
||||||
|
toolbar_offset: Length<f32, DeviceIndependentPixel>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dialog {
|
impl Dialog {
|
||||||
|
@ -119,6 +124,22 @@ impl Dialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_color_picker_dialog(
|
||||||
|
prompt: ColorPicker,
|
||||||
|
toolbar_offset: Length<f32, DeviceIndependentPixel>,
|
||||||
|
) -> Self {
|
||||||
|
let current_color = egui::Color32::from_rgb(
|
||||||
|
prompt.current_color().red,
|
||||||
|
prompt.current_color().green,
|
||||||
|
prompt.current_color().blue,
|
||||||
|
);
|
||||||
|
Dialog::ColorPicker {
|
||||||
|
current_color,
|
||||||
|
maybe_prompt: Some(prompt),
|
||||||
|
toolbar_offset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, ctx: &egui::Context) -> bool {
|
pub fn update(&mut self, ctx: &egui::Context) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Dialog::File {
|
Dialog::File {
|
||||||
|
@ -485,6 +506,50 @@ impl Dialog {
|
||||||
maybe_prompt.take().unwrap().submit();
|
maybe_prompt.take().unwrap().submit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_open
|
||||||
|
},
|
||||||
|
Dialog::ColorPicker {
|
||||||
|
current_color,
|
||||||
|
maybe_prompt,
|
||||||
|
toolbar_offset,
|
||||||
|
} => {
|
||||||
|
let Some(prompt) = maybe_prompt else {
|
||||||
|
// Prompt was dismissed, so the dialog should be closed too.
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut is_open = true;
|
||||||
|
|
||||||
|
let mut position = prompt.position();
|
||||||
|
position.min.y += toolbar_offset.0 as i32;
|
||||||
|
position.max.y += toolbar_offset.0 as i32;
|
||||||
|
let area = egui::Area::new(egui::Id::new("select-window"))
|
||||||
|
.fixed_pos(egui::pos2(position.min.x as f32, position.max.y as f32));
|
||||||
|
|
||||||
|
let modal = Modal::new("select_element_picker".into()).area(area);
|
||||||
|
modal.show(ctx, |ui| {
|
||||||
|
egui::widgets::color_picker::color_picker_color32(
|
||||||
|
ui,
|
||||||
|
current_color,
|
||||||
|
egui::widgets::color_picker::Alpha::Opaque,
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.add_space(10.);
|
||||||
|
|
||||||
|
if ui.button("Dismiss").clicked() {
|
||||||
|
is_open = false;
|
||||||
|
prompt.select(None);
|
||||||
|
}
|
||||||
|
if ui.button("Select").clicked() {
|
||||||
|
is_open = false;
|
||||||
|
let selected_color = RgbColor {
|
||||||
|
red: current_color.r(),
|
||||||
|
green: current_color.g(),
|
||||||
|
blue: current_color.b(),
|
||||||
|
};
|
||||||
|
prompt.select(Some(selected_color));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
is_open
|
is_open
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
2
tests/wpt/meta/css/css-transforms/transform-input-016.html.ini
vendored
Normal file
2
tests/wpt/meta/css/css-transforms/transform-input-016.html.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[transform-input-016.html]
|
||||||
|
expected: FAIL
|
|
@ -38,8 +38,5 @@
|
||||||
[default submit action should supersede input onclick submit() and change the input type from range to submit]
|
[default submit action should supersede input onclick submit() and change the input type from range to submit]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
||||||
[default submit action should supersede input onclick submit() and change the input type from color to submit]
|
|
||||||
expected: FAIL
|
|
||||||
|
|
||||||
[default submit action should supersede input onclick submit() and change the input type from button to submit]
|
[default submit action should supersede input onclick submit() and change the input type from button to submit]
|
||||||
expected: FAIL
|
expected: FAIL
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue