Replace character indices with UTF-8 byte offsets throughout all code dealing with text runs. This eliminates a lot of complexity when converting from one to the other, and interoperates better with the rest of the Rust ecosystem. For most code this is just a simple replacement of char indices with byte indices. In a few places like glyph storage and text fragment scanning, it also lets us get rid of code that existed only to map between bytes and chars. Also includes some related fixes to text shaping, discovered while working on this conversion. See the commit messages for details. r? @pcwalton Source-Repo: https://github.com/servo/servo Source-Revision: cf121ad8dff90b8fa55558ca9bdcbfe29512a617
379 lines
14 KiB
Rust
379 lines
14 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
use dom::attr::{Attr, AttrValue};
|
|
use dom::bindings::cell::DOMRefCell;
|
|
use dom::bindings::codegen::Bindings::EventBinding::EventMethods;
|
|
use dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding;
|
|
use dom::bindings::codegen::Bindings::HTMLTextAreaElementBinding::HTMLTextAreaElementMethods;
|
|
use dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
|
|
use dom::bindings::inheritance::Castable;
|
|
use dom::bindings::js::{LayoutJS, Root};
|
|
use dom::bindings::reflector::{Reflectable};
|
|
use dom::document::Document;
|
|
use dom::element::RawLayoutElementHelpers;
|
|
use dom::element::{AttributeMutation, Element};
|
|
use dom::event::{Event};
|
|
use dom::htmlelement::HTMLElement;
|
|
use dom::htmlfieldsetelement::HTMLFieldSetElement;
|
|
use dom::htmlformelement::{FormControl, HTMLFormElement};
|
|
use dom::htmlinputelement::ChangeEventRunnable;
|
|
use dom::keyboardevent::KeyboardEvent;
|
|
use dom::node::{ChildrenMutation, Node, NodeDamage, UnbindContext};
|
|
use dom::node::{document_from_node};
|
|
use dom::nodelist::NodeList;
|
|
use dom::validation::Validatable;
|
|
use dom::virtualmethods::VirtualMethods;
|
|
use msg::constellation_msg::ConstellationChan;
|
|
use script_traits::ScriptMsg as ConstellationMsg;
|
|
use std::cell::Cell;
|
|
use std::ops::Range;
|
|
use string_cache::Atom;
|
|
use style::element_state::*;
|
|
use textinput::{KeyReaction, Lines, TextInput, SelectionDirection};
|
|
use util::str::DOMString;
|
|
|
|
#[dom_struct]
|
|
pub struct HTMLTextAreaElement {
|
|
htmlelement: HTMLElement,
|
|
#[ignore_heap_size_of = "#7193"]
|
|
textinput: DOMRefCell<TextInput<ConstellationChan<ConstellationMsg>>>,
|
|
// https://html.spec.whatwg.org/multipage/#concept-textarea-dirty
|
|
value_changed: Cell<bool>,
|
|
}
|
|
|
|
pub trait LayoutHTMLTextAreaElementHelpers {
|
|
#[allow(unsafe_code)]
|
|
unsafe fn get_value_for_layout(self) -> String;
|
|
#[allow(unsafe_code)]
|
|
unsafe fn selection_for_layout(self) -> Option<Range<usize>>;
|
|
#[allow(unsafe_code)]
|
|
fn get_cols(self) -> u32;
|
|
#[allow(unsafe_code)]
|
|
fn get_rows(self) -> u32;
|
|
}
|
|
|
|
impl LayoutHTMLTextAreaElementHelpers for LayoutJS<HTMLTextAreaElement> {
|
|
#[allow(unrooted_must_root)]
|
|
#[allow(unsafe_code)]
|
|
unsafe fn get_value_for_layout(self) -> String {
|
|
String::from((*self.unsafe_get()).textinput.borrow_for_layout().get_content())
|
|
}
|
|
|
|
#[allow(unrooted_must_root)]
|
|
#[allow(unsafe_code)]
|
|
unsafe fn selection_for_layout(self) -> Option<Range<usize>> {
|
|
if !(*self.unsafe_get()).upcast::<Element>().focus_state() {
|
|
return None;
|
|
}
|
|
let textinput = (*self.unsafe_get()).textinput.borrow_for_layout();
|
|
Some(textinput.get_absolute_selection_range())
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
fn get_cols(self) -> u32 {
|
|
unsafe {
|
|
(*self.upcast::<Element>().unsafe_get())
|
|
.get_attr_for_layout(&ns!(), &atom!("cols"))
|
|
.map_or(DEFAULT_COLS, AttrValue::as_uint)
|
|
}
|
|
}
|
|
|
|
#[allow(unsafe_code)]
|
|
fn get_rows(self) -> u32 {
|
|
unsafe {
|
|
(*self.upcast::<Element>().unsafe_get())
|
|
.get_attr_for_layout(&ns!(), &atom!("rows"))
|
|
.map_or(DEFAULT_ROWS, AttrValue::as_uint)
|
|
}
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#attr-textarea-cols-value
|
|
static DEFAULT_COLS: u32 = 20;
|
|
|
|
// https://html.spec.whatwg.org/multipage/#attr-textarea-rows-value
|
|
static DEFAULT_ROWS: u32 = 2;
|
|
|
|
impl HTMLTextAreaElement {
|
|
fn new_inherited(localName: Atom,
|
|
prefix: Option<DOMString>,
|
|
document: &Document) -> HTMLTextAreaElement {
|
|
let chan = document.window().constellation_chan();
|
|
HTMLTextAreaElement {
|
|
htmlelement:
|
|
HTMLElement::new_inherited_with_state(IN_ENABLED_STATE,
|
|
localName, prefix, document),
|
|
textinput: DOMRefCell::new(TextInput::new(
|
|
Lines::Multiple, DOMString::new(), chan, None, SelectionDirection::None)),
|
|
value_changed: Cell::new(false),
|
|
}
|
|
}
|
|
|
|
#[allow(unrooted_must_root)]
|
|
pub fn new(localName: Atom,
|
|
prefix: Option<DOMString>,
|
|
document: &Document) -> Root<HTMLTextAreaElement> {
|
|
let element = HTMLTextAreaElement::new_inherited(localName, prefix, document);
|
|
Node::reflect_node(box element, document, HTMLTextAreaElementBinding::Wrap)
|
|
}
|
|
}
|
|
|
|
impl HTMLTextAreaElementMethods for HTMLTextAreaElement {
|
|
// TODO A few of these attributes have default values and additional
|
|
// constraints
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-cols
|
|
make_uint_getter!(Cols, "cols", DEFAULT_COLS);
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-cols
|
|
make_limited_uint_setter!(SetCols, "cols", DEFAULT_COLS);
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-fe-disabled
|
|
make_bool_getter!(Disabled, "disabled");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-fe-disabled
|
|
make_bool_setter!(SetDisabled, "disabled");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-fae-form
|
|
fn GetForm(&self) -> Option<Root<HTMLFormElement>> {
|
|
self.form_owner()
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#attr-fe-name
|
|
make_getter!(Name, "name");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#attr-fe-name
|
|
make_setter!(SetName, "name");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-placeholder
|
|
make_getter!(Placeholder, "placeholder");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-placeholder
|
|
make_setter!(SetPlaceholder, "placeholder");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#attr-textarea-readonly
|
|
make_bool_getter!(ReadOnly, "readonly");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#attr-textarea-readonly
|
|
make_bool_setter!(SetReadOnly, "readonly");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-required
|
|
make_bool_getter!(Required, "required");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-required
|
|
make_bool_setter!(SetRequired, "required");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-rows
|
|
make_uint_getter!(Rows, "rows", DEFAULT_ROWS);
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-rows
|
|
make_limited_uint_setter!(SetRows, "rows", DEFAULT_ROWS);
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-wrap
|
|
make_getter!(Wrap, "wrap");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-wrap
|
|
make_setter!(SetWrap, "wrap");
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-type
|
|
fn Type(&self) -> DOMString {
|
|
DOMString::from("textarea")
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-defaultvalue
|
|
fn DefaultValue(&self) -> DOMString {
|
|
self.upcast::<Node>().GetTextContent().unwrap()
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-defaultvalue
|
|
fn SetDefaultValue(&self, value: DOMString) {
|
|
self.upcast::<Node>().SetTextContent(Some(value));
|
|
|
|
// if the element's dirty value flag is false, then the element's
|
|
// raw value must be set to the value of the element's textContent IDL attribute
|
|
if !self.value_changed.get() {
|
|
self.reset();
|
|
}
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-value
|
|
fn Value(&self) -> DOMString {
|
|
self.textinput.borrow().get_content()
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea-value
|
|
fn SetValue(&self, value: DOMString) {
|
|
// TODO move the cursor to the end of the field
|
|
self.textinput.borrow_mut().set_content(value);
|
|
self.value_changed.set(true);
|
|
|
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-lfe-labels
|
|
fn Labels(&self) -> Root<NodeList> {
|
|
self.upcast::<HTMLElement>().labels()
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
|
|
fn SetSelectionDirection(&self, direction: DOMString) {
|
|
self.textinput.borrow_mut().selection_direction = SelectionDirection::from(direction);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectiondirection
|
|
fn SelectionDirection(&self) -> DOMString {
|
|
DOMString::from(self.textinput.borrow().selection_direction)
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
|
|
fn SetSelectionEnd(&self, end: u32) {
|
|
let selection_start = self.SelectionStart();
|
|
self.textinput.borrow_mut().set_selection_range(selection_start, end);
|
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionend
|
|
fn SelectionEnd(&self) -> u32 {
|
|
self.textinput.borrow().get_absolute_insertion_point() as u32
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
|
|
fn SetSelectionStart(&self, start: u32) {
|
|
let selection_end = self.SelectionEnd();
|
|
self.textinput.borrow_mut().set_selection_range(start, selection_end);
|
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-selectionstart
|
|
fn SelectionStart(&self) -> u32 {
|
|
self.textinput.borrow().get_selection_start()
|
|
}
|
|
|
|
// https://html.spec.whatwg.org/multipage/#dom-textarea/input-setselectionrange
|
|
fn SetSelectionRange(&self, start: u32, end: u32, direction: Option<DOMString>) {
|
|
let direction = direction.map_or(SelectionDirection::None, |d| SelectionDirection::from(d));
|
|
self.textinput.borrow_mut().selection_direction = direction;
|
|
self.textinput.borrow_mut().set_selection_range(start, end);
|
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
}
|
|
}
|
|
|
|
|
|
impl HTMLTextAreaElement {
|
|
// https://html.spec.whatwg.org/multipage/#concept-fe-mutable
|
|
pub fn mutable(&self) -> bool {
|
|
// https://html.spec.whatwg.org/multipage/#the-textarea-element:concept-fe-mutable
|
|
!(self.Disabled() || self.ReadOnly())
|
|
}
|
|
pub fn reset(&self) {
|
|
// https://html.spec.whatwg.org/multipage/#the-textarea-element:concept-form-reset-control
|
|
self.SetValue(self.DefaultValue());
|
|
self.value_changed.set(false);
|
|
}
|
|
}
|
|
|
|
|
|
impl VirtualMethods for HTMLTextAreaElement {
|
|
fn super_type(&self) -> Option<&VirtualMethods> {
|
|
Some(self.upcast::<HTMLElement>() as &VirtualMethods)
|
|
}
|
|
|
|
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation) {
|
|
self.super_type().unwrap().attribute_mutated(attr, mutation);
|
|
match *attr.local_name() {
|
|
atom!("disabled") => {
|
|
let el = self.upcast::<Element>();
|
|
match mutation {
|
|
AttributeMutation::Set(_) => {
|
|
el.set_disabled_state(true);
|
|
el.set_enabled_state(false);
|
|
},
|
|
AttributeMutation::Removed => {
|
|
el.set_disabled_state(false);
|
|
el.set_enabled_state(true);
|
|
el.check_ancestors_disabled_state_for_form_control();
|
|
}
|
|
}
|
|
},
|
|
_ => {},
|
|
}
|
|
}
|
|
|
|
fn bind_to_tree(&self, tree_in_doc: bool) {
|
|
if let Some(ref s) = self.super_type() {
|
|
s.bind_to_tree(tree_in_doc);
|
|
}
|
|
|
|
self.upcast::<Element>().check_ancestors_disabled_state_for_form_control();
|
|
}
|
|
|
|
fn parse_plain_attribute(&self, name: &Atom, value: DOMString) -> AttrValue {
|
|
match *name {
|
|
atom!("cols") => AttrValue::from_limited_u32(value, DEFAULT_COLS),
|
|
atom!("rows") => AttrValue::from_limited_u32(value, DEFAULT_ROWS),
|
|
_ => self.super_type().unwrap().parse_plain_attribute(name, value),
|
|
}
|
|
}
|
|
|
|
fn unbind_from_tree(&self, context: &UnbindContext) {
|
|
self.super_type().unwrap().unbind_from_tree(context);
|
|
|
|
let node = self.upcast::<Node>();
|
|
let el = self.upcast::<Element>();
|
|
if node.ancestors().any(|ancestor| ancestor.is::<HTMLFieldSetElement>()) {
|
|
el.check_ancestors_disabled_state_for_form_control();
|
|
} else {
|
|
el.check_disabled_attribute();
|
|
}
|
|
}
|
|
|
|
fn children_changed(&self, mutation: &ChildrenMutation) {
|
|
if let Some(ref s) = self.super_type() {
|
|
s.children_changed(mutation);
|
|
}
|
|
if !self.value_changed.get() {
|
|
self.reset();
|
|
}
|
|
}
|
|
|
|
// copied and modified from htmlinputelement.rs
|
|
fn handle_event(&self, event: &Event) {
|
|
if let Some(s) = self.super_type() {
|
|
s.handle_event(event);
|
|
}
|
|
|
|
if event.type_() == atom!("click") && !event.DefaultPrevented() {
|
|
//TODO: set the editing position for text inputs
|
|
|
|
document_from_node(self).request_focus(self.upcast());
|
|
} else if event.type_() == atom!("keydown") && !event.DefaultPrevented() {
|
|
if let Some(kevent) = event.downcast::<KeyboardEvent>() {
|
|
match self.textinput.borrow_mut().handle_keydown(kevent) {
|
|
KeyReaction::TriggerDefaultAction => (),
|
|
KeyReaction::DispatchInput => {
|
|
self.value_changed.set(true);
|
|
|
|
if event.IsTrusted() {
|
|
ChangeEventRunnable::send(self.upcast::<Node>());
|
|
}
|
|
|
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
event.PreventDefault();
|
|
}
|
|
KeyReaction::RedrawSelection => {
|
|
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
|
event.PreventDefault();
|
|
}
|
|
KeyReaction::Nothing => (),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FormControl for HTMLTextAreaElement {}
|
|
|
|
impl Validatable for HTMLTextAreaElement {}
|