diff --git a/components/layout/construct.rs b/components/layout/construct.rs index 25988b7eb11..cc40db0174d 100644 --- a/components/layout/construct.rs +++ b/components/layout/construct.rs @@ -709,13 +709,13 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode> return } - let insertion_point = node.insertion_point(); + let selection = node.selection(); let mut style = (*style).clone(); properties::modify_style_for_text(&mut style); match text_content { TextContent::Text(string) => { - let info = UnscannedTextFragmentInfo::new(string, insertion_point); + let info = UnscannedTextFragmentInfo::new(string, selection); let specific_fragment_info = SpecificFragmentInfo::UnscannedText(info); fragments.fragments.push_back(Fragment::from_opaque_node_and_style( node.opaque(), diff --git a/components/layout/display_list_builder.rs b/components/layout/display_list_builder.rs index 4d62d221a5e..a55a1f2bdc3 100644 --- a/components/layout/display_list_builder.rs +++ b/components/layout/display_list_builder.rs @@ -104,6 +104,10 @@ impl<'a> DisplayListBuildState<'a> { /// The logical width of an insertion point: at the moment, a one-pixel-wide line. const INSERTION_POINT_LOGICAL_WIDTH: Au = Au(1 * AU_PER_PX); +// Colors for selected text. TODO (#8077): Use the ::selection pseudo-element to set these. +const SELECTION_FOREGROUND_COLOR: RGBA = RGBA { red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0 }; +const SELECTION_BACKGROUND_COLOR: RGBA = RGBA { red: 1.0, green: 0.5, blue: 0.0, alpha: 1.0 }; + // TODO(gw): The transforms spec says that perspective length must // be positive. However, there is some confusion between the spec // and browser implementations as to handling the case of 0 for the @@ -922,6 +926,23 @@ impl FragmentDisplayListBuilding for Fragment { } _ => return, }; + + // Draw a highlighted background if the text is selected. + // + // TODO: Allow non-text fragments to be selected too. + if scanned_text_fragment_info.selected { + state.add_display_item( + DisplayItem::SolidColorClass(box SolidColorDisplayItem { + base: BaseDisplayItem::new(stacking_relative_border_box, + DisplayItemMetadata::new(self.node, + &*self.style, + Cursor::DefaultCursor), + &clip), + color: SELECTION_BACKGROUND_COLOR.to_gfx_color() + }), display_list_section); + } + + // Draw a caret at the insertion point. let insertion_point_index = match scanned_text_fragment_info.insertion_point { Some(insertion_point_index) => insertion_point_index, None => return, @@ -1095,7 +1116,11 @@ impl FragmentDisplayListBuilding for Fragment { // // NB: According to CSS-BACKGROUNDS, text shadows render in *reverse* order (front // to back). - let text_color = self.style().get_color().color; + let text_color = if text_fragment.selected { + SELECTION_FOREGROUND_COLOR + } else { + self.style().get_color().color + }; for text_shadow in self.style.get_effects().text_shadow.0.iter().rev() { let offset = &Point2D::new(text_shadow.offset_x, text_shadow.offset_y); let color = self.style().resolve_color(text_shadow.color); diff --git a/components/layout/fragment.rs b/components/layout/fragment.rs index 8c1c54fd282..a5197af7d46 100644 --- a/components/layout/fragment.rs +++ b/components/layout/fragment.rs @@ -655,10 +655,11 @@ pub struct ScannedTextFragmentInfo { pub content_size: LogicalSize, /// The position of the insertion point in characters, if any. - /// - /// TODO(pcwalton): Make this a range. pub insertion_point: Option, + /// Is this fragment selected? + pub selected: bool, + /// The range within the above text run that this represents. pub range: Range, @@ -677,13 +678,15 @@ impl ScannedTextFragmentInfo { pub fn new(run: Arc, range: Range, content_size: LogicalSize, - insertion_point: &Option, + insertion_point: Option, + selected: bool, requires_line_break_afterward_if_wrapping_on_newlines: bool) -> ScannedTextFragmentInfo { ScannedTextFragmentInfo { run: run, range: range, - insertion_point: *insertion_point, + insertion_point: insertion_point, + selected: selected, content_size: content_size, range_end_including_stripped_whitespace: range.end(), requires_line_break_afterward_if_wrapping_on_newlines: @@ -737,19 +740,17 @@ pub struct UnscannedTextFragmentInfo { /// The text inside the fragment. pub text: Box, - /// The position of the insertion point, if any. - /// - /// TODO(pcwalton): Make this a range. - pub insertion_point: Option, + /// The selected text range. An empty range represents the insertion point. + pub selection: Option>, } impl UnscannedTextFragmentInfo { /// Creates a new instance of `UnscannedTextFragmentInfo` from the given text. #[inline] - pub fn new(text: String, insertion_point: Option) -> UnscannedTextFragmentInfo { + pub fn new(text: String, selection: Option>) -> UnscannedTextFragmentInfo { UnscannedTextFragmentInfo { text: text.into_boxed_str(), - insertion_point: insertion_point, + selection: selection, } } } @@ -872,7 +873,8 @@ impl Fragment { text_run, split.range, size, - &None, + None, + false, requires_line_break_afterward_if_wrapping_on_newlines); self.transform(size, SpecificFragmentInfo::ScannedText(info)) } diff --git a/components/layout/inline.rs b/components/layout/inline.rs index 8784728d808..6f102f268fa 100644 --- a/components/layout/inline.rs +++ b/components/layout/inline.rs @@ -354,6 +354,7 @@ impl LineBreaker { let need_to_merge = match (&mut result.specific, &candidate.specific) { (&mut SpecificFragmentInfo::ScannedText(ref mut result_info), &SpecificFragmentInfo::ScannedText(ref candidate_info)) => { + result_info.selected == candidate_info.selected && util::arc_ptr_eq(&result_info.run, &candidate_info.run) && inline_contexts_are_equal(&result.inline_context, &candidate.inline_context) diff --git a/components/layout/text.rs b/components/layout/text.rs index 2a9e99d18d3..4e339c28a2c 100644 --- a/components/layout/text.rs +++ b/components/layout/text.rs @@ -172,17 +172,21 @@ impl TextRunScanner { for (fragment_index, in_fragment) in self.clump.iter().enumerate() { let mut mapping = RunMapping::new(&run_info_list[..], &run_info, fragment_index); let text; - let insertion_point; + let selection; match in_fragment.specific { SpecificFragmentInfo::UnscannedText(ref text_fragment_info) => { text = &text_fragment_info.text; - insertion_point = text_fragment_info.insertion_point; + selection = text_fragment_info.selection; } _ => panic!("Expected an unscanned text fragment!"), }; + let insertion_point = match selection { + Some(range) if range.is_empty() => Some(range.begin()), + _ => None + }; let (mut start_position, mut end_position) = (0, 0); - for character in text.chars() { + for (char_index, character) in text.chars().enumerate() { // Search for the first font in this font group that contains a glyph for this // character. let mut font_index = 0; @@ -213,11 +217,18 @@ impl TextRunScanner { run_info.script = script; } + let selected = match selection { + Some(range) => range.contains(CharIndex(char_index as isize)), + None => false + }; + // Now, if necessary, flush the mapping we were building up. - if run_info.font_index != font_index || - run_info.bidi_level != bidi_level || - !compatible_script - { + let flush_run = run_info.font_index != font_index || + run_info.bidi_level != bidi_level || + !compatible_script; + let flush_mapping = flush_run || mapping.selected != selected; + + if flush_mapping { if end_position > start_position { mapping.flush(&mut mappings, &mut run_info, @@ -230,8 +241,10 @@ impl TextRunScanner { end_position); } if run_info.text.len() > 0 { - run_info_list.push(run_info); - run_info = RunInfo::new(); + if flush_run { + run_info_list.push(run_info); + run_info = RunInfo::new(); + } mapping = RunMapping::new(&run_info_list[..], &run_info, fragment_index); @@ -239,6 +252,7 @@ impl TextRunScanner { run_info.font_index = font_index; run_info.bidi_level = bidi_level; run_info.script = script; + mapping.selected = selected; } // Consume this character. @@ -334,7 +348,8 @@ impl TextRunScanner { scanned_run.run, mapping.char_range, text_size, - &scanned_run.insertion_point, + scanned_run.insertion_point, + mapping.selected, requires_line_break_afterward_if_wrapping_on_newlines); let new_metrics = new_text_fragment_info.run.metrics_for_range(&mapping.char_range); @@ -408,7 +423,7 @@ fn split_first_fragment_at_newline_if_necessary(fragments: &mut LinkedList= offset => { - insertion_point_before = None; - unscanned_text_fragment_info.insertion_point = Some(insertion_point - offset); + match unscanned_text_fragment_info.selection { + Some(ref mut selection) if selection.begin() >= offset => { + // Selection is entirely in the second fragment. + selection_before = None; + selection.shift_by(-offset); } - Some(_) | None => { - insertion_point_before = unscanned_text_fragment_info.insertion_point; - unscanned_text_fragment_info.insertion_point = None; + Some(ref mut selection) if selection.end() > offset => { + // Selection is split across two fragments. + selection_before = Some(Range::new(selection.begin(), offset)); + *selection = Range::new(CharIndex(0), selection.end() - offset); + } + _ => { + // Selection is entirely in the first fragment. + selection_before = unscanned_text_fragment_info.selection; + unscanned_text_fragment_info.selection = None; } }; } first_fragment.transform(first_fragment.border_box.size, SpecificFragmentInfo::UnscannedText( UnscannedTextFragmentInfo::new(string_before, - insertion_point_before))) + selection_before))) }; fragments.push_front(new_fragment); @@ -494,6 +516,8 @@ struct RunMapping { old_fragment_index: usize, /// The index of the text run we're going to create. text_run_index: usize, + /// Is the text in this fragment selected? + selected: bool, } impl RunMapping { @@ -508,6 +532,7 @@ impl RunMapping { byte_range: Range::new(0, 0), old_fragment_index: fragment_index, text_run_index: run_info_list.len(), + selected: false, } } diff --git a/components/layout/wrapper.rs b/components/layout/wrapper.rs index 30ca65f6e39..0eb651a645b 100644 --- a/components/layout/wrapper.rs +++ b/components/layout/wrapper.rs @@ -37,6 +37,7 @@ use gfx::text::glyph::CharIndex; use incremental::RestyleDamage; use msg::constellation_msg::PipelineId; use opaque_node::OpaqueNodeMethods; +use range::{Range, RangeIndex}; use script::dom::attr::AttrValue; use script::dom::bindings::inheritance::{Castable, CharacterDataTypeId, ElementTypeId}; use script::dom::bindings::inheritance::{HTMLElementTypeId, NodeTypeId}; @@ -825,7 +826,7 @@ pub trait ThreadSafeLayoutNode : Clone + Copy + Sized + PartialEq { fn text_content(&self) -> TextContent; /// If the insertion point is within this node, returns it. Otherwise, returns `None`. - fn insertion_point(&self) -> Option; + fn selection(&self) -> Option>; /// If this is an image element, returns its URL. If this is not an image element, fails. /// @@ -1049,21 +1050,24 @@ impl<'ln> ThreadSafeLayoutNode for ServoThreadSafeLayoutNode<'ln> { panic!("not text!") } - fn insertion_point(&self) -> Option { + fn selection(&self) -> Option> { let this = unsafe { self.get_jsmanaged() }; if let Some(area) = this.downcast::() { - if let Some(insertion_point) = unsafe { area.get_absolute_insertion_point_for_layout() } { + if let Some(selection) = unsafe { area.get_absolute_selection_for_layout() } { let text = unsafe { area.get_value_for_layout() }; - return Some(CharIndex(search_index(insertion_point, text.char_indices()))); + let begin_byte = selection.begin(); + let begin = search_index(begin_byte, text.char_indices()); + let length = search_index(selection.length(), text[begin_byte..].char_indices()); + return Some(Range::new(CharIndex(begin), CharIndex(length))); } } if let Some(input) = this.downcast::() { - let insertion_point_index = unsafe { input.get_insertion_point_index_for_layout() }; - if let Some(insertion_point_index) = insertion_point_index { - return Some(CharIndex(insertion_point_index)); + if let Some(selection) = unsafe { input.get_selection_for_layout() } { + return Some(Range::new(CharIndex(selection.begin()), + CharIndex(selection.length()))); } } None diff --git a/components/script/Cargo.toml b/components/script/Cargo.toml index cb3a8d81181..bf255df7362 100644 --- a/components/script/Cargo.toml +++ b/components/script/Cargo.toml @@ -82,6 +82,7 @@ num = "0.1.24" rand = "0.3" phf = "0.7.13" phf_macros = "0.7.13" +range = { path = "../range" } ref_filter_map = "1.0" ref_slice = "0.1.0" regex = "0.1.43" diff --git a/components/script/dom/htmlinputelement.rs b/components/script/dom/htmlinputelement.rs index d003456e648..f75fd670465 100644 --- a/components/script/dom/htmlinputelement.rs +++ b/components/script/dom/htmlinputelement.rs @@ -31,6 +31,7 @@ use dom::nodelist::NodeList; use dom::validation::Validatable; use dom::virtualmethods::VirtualMethods; use msg::constellation_msg::ConstellationChan; +use range::Range; use script_thread::ScriptThreadEventCategory::InputEvent; use script_thread::{CommonScriptMsg, Runnable}; use script_traits::ScriptMsg as ConstellationMsg; @@ -209,7 +210,7 @@ pub trait LayoutHTMLInputElementHelpers { #[allow(unsafe_code)] unsafe fn get_size_for_layout(self) -> u32; #[allow(unsafe_code)] - unsafe fn get_insertion_point_index_for_layout(self) -> Option; + unsafe fn get_selection_for_layout(self) -> Option>; #[allow(unsafe_code)] unsafe fn get_checked_state_for_layout(self) -> bool; #[allow(unsafe_code)] @@ -242,7 +243,7 @@ impl LayoutHTMLInputElementHelpers for LayoutJS { InputType::InputPassword => { let text = get_raw_textinput_value(self); if !text.is_empty() { - // The implementation of get_insertion_point_index_for_layout expects a 1:1 mapping of chars. + // The implementation of get_selection_for_layout expects a 1:1 mapping of chars. text.chars().map(|_| '●').collect() } else { String::from((*self.unsafe_get()).placeholder.borrow_for_layout().clone()) @@ -251,7 +252,7 @@ impl LayoutHTMLInputElementHelpers for LayoutJS { _ => { let text = get_raw_textinput_value(self); if !text.is_empty() { - // The implementation of get_insertion_point_index_for_layout expects a 1:1 mapping of chars. + // The implementation of get_selection_for_layout expects a 1:1 mapping of chars. String::from(text) } else { String::from((*self.unsafe_get()).placeholder.borrow_for_layout().clone()) @@ -268,25 +269,24 @@ impl LayoutHTMLInputElementHelpers for LayoutJS { #[allow(unrooted_must_root)] #[allow(unsafe_code)] - unsafe fn get_insertion_point_index_for_layout(self) -> Option { + unsafe fn get_selection_for_layout(self) -> Option> { if !(*self.unsafe_get()).upcast::().get_focus_state() { return None; } - match (*self.unsafe_get()).input_type.get() { - InputType::InputText => { - let raw = self.get_value_for_layout(); - Some(search_index((*self.unsafe_get()).textinput.borrow_for_layout().edit_point.index, - raw.char_indices())) - } - InputType::InputPassword => { - // Use the raw textinput to get the index as long as we use a 1:1 char mapping - // in get_input_value_for_layout. - let raw = get_raw_textinput_value(self); - Some(search_index((*self.unsafe_get()).textinput.borrow_for_layout().edit_point.index, - raw.char_indices())) - } - _ => None - } + + // Use the raw textinput to get the index as long as we use a 1:1 char mapping + // in get_value_for_layout. + let raw = match (*self.unsafe_get()).input_type.get() { + InputType::InputText | + InputType::InputPassword => get_raw_textinput_value(self), + _ => return None + }; + let textinput = (*self.unsafe_get()).textinput.borrow_for_layout(); + let selection = textinput.get_absolute_selection_range(); + let begin_byte = selection.begin(); + let begin = search_index(begin_byte, raw.char_indices()); + let length = search_index(selection.length(), raw[begin_byte..].char_indices()); + Some(Range::new(begin, length)) } #[allow(unrooted_must_root)] diff --git a/components/script/dom/htmltextareaelement.rs b/components/script/dom/htmltextareaelement.rs index c2289e176da..0f82919124f 100644 --- a/components/script/dom/htmltextareaelement.rs +++ b/components/script/dom/htmltextareaelement.rs @@ -26,6 +26,7 @@ use dom::nodelist::NodeList; use dom::validation::Validatable; use dom::virtualmethods::VirtualMethods; use msg::constellation_msg::ConstellationChan; +use range::Range; use script_traits::ScriptMsg as ConstellationMsg; use std::cell::Cell; use string_cache::Atom; @@ -46,7 +47,7 @@ pub trait LayoutHTMLTextAreaElementHelpers { #[allow(unsafe_code)] unsafe fn get_value_for_layout(self) -> String; #[allow(unsafe_code)] - unsafe fn get_absolute_insertion_point_for_layout(self) -> Option; + unsafe fn get_absolute_selection_for_layout(self) -> Option>; #[allow(unsafe_code)] fn get_cols(self) -> u32; #[allow(unsafe_code)] @@ -62,10 +63,10 @@ impl LayoutHTMLTextAreaElementHelpers for LayoutJS { #[allow(unrooted_must_root)] #[allow(unsafe_code)] - unsafe fn get_absolute_insertion_point_for_layout(self) -> Option { + unsafe fn get_absolute_selection_for_layout(self) -> Option> { if (*self.unsafe_get()).upcast::().get_focus_state() { Some((*self.unsafe_get()).textinput.borrow_for_layout() - .get_absolute_insertion_point()) + .get_absolute_selection_range()) } else { None } diff --git a/components/script/lib.rs b/components/script/lib.rs index 99a3bb7ce9b..feb7a5fee26 100644 --- a/components/script/lib.rs +++ b/components/script/lib.rs @@ -58,6 +58,7 @@ extern crate phf; #[macro_use] extern crate profile_traits; extern crate rand; +extern crate range; extern crate ref_filter_map; extern crate ref_slice; extern crate regex; diff --git a/components/script/textinput.rs b/components/script/textinput.rs index 35b49ee57a4..343b114bf2d 100644 --- a/components/script/textinput.rs +++ b/components/script/textinput.rs @@ -8,6 +8,7 @@ use clipboard_provider::ClipboardProvider; use dom::keyboardevent::{KeyboardEvent, key_value}; use msg::constellation_msg::{ALT, CONTROL, SHIFT, SUPER}; use msg::constellation_msg::{Key, KeyModifiers}; +use range::Range; use std::borrow::ToOwned; use std::cmp::{max, min}; use std::default::Default; @@ -154,6 +155,15 @@ impl TextInput { }) } + pub fn get_absolute_selection_range(&self) -> Range { + match self.get_sorted_selection() { + Some((begin, _end)) => + Range::new(self.get_absolute_point_for_text_point(&begin), self.selection_len()), + None => + Range::new(self.get_absolute_insertion_point(), 0) + } + } + pub fn get_selection_text(&self) -> Option { self.get_sorted_selection().map(|(begin, end)| { if begin.line != end.line { diff --git a/components/servo/Cargo.lock b/components/servo/Cargo.lock index 9d4b8f7034c..e62ff7d7e2d 100644 --- a/components/servo/Cargo.lock +++ b/components/servo/Cargo.lock @@ -1716,6 +1716,7 @@ dependencies = [ "plugins 0.0.1", "profile_traits 0.0.1", "rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "range 0.0.1", "ref_filter_map 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "ref_slice 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/ports/cef/Cargo.lock b/ports/cef/Cargo.lock index bd94cad6d47..54c664fe827 100644 --- a/ports/cef/Cargo.lock +++ b/ports/cef/Cargo.lock @@ -1591,6 +1591,7 @@ dependencies = [ "plugins 0.0.1", "profile_traits 0.0.1", "rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "range 0.0.1", "ref_filter_map 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "ref_slice 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/ports/gonk/Cargo.lock b/ports/gonk/Cargo.lock index 2ad85f8a85a..f28d6b49896 100644 --- a/ports/gonk/Cargo.lock +++ b/ports/gonk/Cargo.lock @@ -1573,6 +1573,7 @@ dependencies = [ "plugins 0.0.1", "profile_traits 0.0.1", "rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "range 0.0.1", "ref_filter_map 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "ref_slice 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index 73d57a5c311..a5acda41d1b 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -2528,6 +2528,30 @@ "url": "/_mozilla/css/input_placeholder_ref.html" } ], + "css/input_selection_a.html": [ + { + "path": "css/input_selection_a.html", + "references": [ + [ + "/_mozilla/css/input_selection_ref.html", + "==" + ] + ], + "url": "/_mozilla/css/input_selection_a.html" + } + ], + "css/input_selection_ref.html": [ + { + "path": "css/input_selection_ref.html", + "references": [ + [ + "/_mozilla/css/input_selection_ref.html", + "==" + ] + ], + "url": "/_mozilla/css/input_selection_ref.html" + } + ], "css/input_whitespace.html": [ { "path": "css/input_whitespace.html", @@ -8818,6 +8842,30 @@ "url": "/_mozilla/css/input_placeholder_ref.html" } ], + "css/input_selection_a.html": [ + { + "path": "css/input_selection_a.html", + "references": [ + [ + "/_mozilla/css/input_selection_ref.html", + "==" + ] + ], + "url": "/_mozilla/css/input_selection_a.html" + } + ], + "css/input_selection_ref.html": [ + { + "path": "css/input_selection_ref.html", + "references": [ + [ + "/_mozilla/css/input_selection_ref.html", + "==" + ] + ], + "url": "/_mozilla/css/input_selection_ref.html" + } + ], "css/input_whitespace.html": [ { "path": "css/input_whitespace.html", diff --git a/tests/wpt/mozilla/tests/css/input_selection_a.html b/tests/wpt/mozilla/tests/css/input_selection_a.html new file mode 100644 index 00000000000..0e923f9425f --- /dev/null +++ b/tests/wpt/mozilla/tests/css/input_selection_a.html @@ -0,0 +1,28 @@ + + + + + input selection test + + + + + + + + diff --git a/tests/wpt/mozilla/tests/css/input_selection_ref.html b/tests/wpt/mozilla/tests/css/input_selection_ref.html new file mode 100644 index 00000000000..6903f7d9118 --- /dev/null +++ b/tests/wpt/mozilla/tests/css/input_selection_ref.html @@ -0,0 +1,18 @@ + + + + + input selection test + + + + + Hello + +