Auto merge of #10176 - mbrubeck:selection-range, r=pcwalton

Highlight selected text in input fields

Fixes #9993.  This does not yet allow stylesheets to set the selection colors; instead it uses a hard-coded orange background and white foreground.

r? @pcwalton

<!-- Reviewable:start -->
---
This change is [<img src="https://reviewable.io/review_button.svg" height="35" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/servo/servo/10176)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2016-03-26 08:16:20 +05:30
commit bed91b3334
17 changed files with 264 additions and 77 deletions

View file

@ -709,13 +709,13 @@ impl<'a, ConcreteThreadSafeLayoutNode: ThreadSafeLayoutNode>
return return
} }
let insertion_point = node.insertion_point(); let selection = node.selection();
let mut style = (*style).clone(); let mut style = (*style).clone();
properties::modify_style_for_text(&mut style); properties::modify_style_for_text(&mut style);
match text_content { match text_content {
TextContent::Text(string) => { TextContent::Text(string) => {
let info = UnscannedTextFragmentInfo::new(string, insertion_point); let info = UnscannedTextFragmentInfo::new(string, selection);
let specific_fragment_info = SpecificFragmentInfo::UnscannedText(info); let specific_fragment_info = SpecificFragmentInfo::UnscannedText(info);
fragments.fragments.push_back(Fragment::from_opaque_node_and_style( fragments.fragments.push_back(Fragment::from_opaque_node_and_style(
node.opaque(), node.opaque(),

View file

@ -104,6 +104,10 @@ impl<'a> DisplayListBuildState<'a> {
/// The logical width of an insertion point: at the moment, a one-pixel-wide line. /// 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); 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 // TODO(gw): The transforms spec says that perspective length must
// be positive. However, there is some confusion between the spec // be positive. However, there is some confusion between the spec
// and browser implementations as to handling the case of 0 for the // and browser implementations as to handling the case of 0 for the
@ -922,6 +926,23 @@ impl FragmentDisplayListBuilding for Fragment {
} }
_ => return, _ => 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 { let insertion_point_index = match scanned_text_fragment_info.insertion_point {
Some(insertion_point_index) => insertion_point_index, Some(insertion_point_index) => insertion_point_index,
None => return, None => return,
@ -1095,7 +1116,11 @@ impl FragmentDisplayListBuilding for Fragment {
// //
// NB: According to CSS-BACKGROUNDS, text shadows render in *reverse* order (front // NB: According to CSS-BACKGROUNDS, text shadows render in *reverse* order (front
// to back). // 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() { 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 offset = &Point2D::new(text_shadow.offset_x, text_shadow.offset_y);
let color = self.style().resolve_color(text_shadow.color); let color = self.style().resolve_color(text_shadow.color);

View file

@ -655,8 +655,6 @@ pub struct ScannedTextFragmentInfo {
pub content_size: LogicalSize<Au>, pub content_size: LogicalSize<Au>,
/// The position of the insertion point in characters, if any. /// The position of the insertion point in characters, if any.
///
/// TODO(pcwalton): Make this a range.
pub insertion_point: Option<CharIndex>, pub insertion_point: Option<CharIndex>,
/// The range within the above text run that this represents. /// The range within the above text run that this represents.
@ -667,9 +665,18 @@ pub struct ScannedTextFragmentInfo {
/// performing incremental reflow. /// performing incremental reflow.
pub range_end_including_stripped_whitespace: CharIndex, pub range_end_including_stripped_whitespace: CharIndex,
/// Whether a line break is required after this fragment if wrapping on newlines (e.g. if pub flags: ScannedTextFlags,
/// `white-space: pre` is in effect). }
pub requires_line_break_afterward_if_wrapping_on_newlines: bool,
bitflags! {
flags ScannedTextFlags: u8 {
/// Whether a line break is required after this fragment if wrapping on newlines (e.g. if
/// `white-space: pre` is in effect).
const REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES = 0x01,
/// Is this fragment selected?
const SELECTED = 0x02,
}
} }
impl ScannedTextFragmentInfo { impl ScannedTextFragmentInfo {
@ -677,19 +684,26 @@ impl ScannedTextFragmentInfo {
pub fn new(run: Arc<TextRun>, pub fn new(run: Arc<TextRun>,
range: Range<CharIndex>, range: Range<CharIndex>,
content_size: LogicalSize<Au>, content_size: LogicalSize<Au>,
insertion_point: &Option<CharIndex>, insertion_point: Option<CharIndex>,
requires_line_break_afterward_if_wrapping_on_newlines: bool) flags: ScannedTextFlags)
-> ScannedTextFragmentInfo { -> ScannedTextFragmentInfo {
ScannedTextFragmentInfo { ScannedTextFragmentInfo {
run: run, run: run,
range: range, range: range,
insertion_point: *insertion_point, insertion_point: insertion_point,
content_size: content_size, content_size: content_size,
range_end_including_stripped_whitespace: range.end(), range_end_including_stripped_whitespace: range.end(),
requires_line_break_afterward_if_wrapping_on_newlines: flags: flags,
requires_line_break_afterward_if_wrapping_on_newlines,
} }
} }
pub fn requires_line_break_afterward_if_wrapping_on_newlines(&self) -> bool {
self.flags.contains(REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES)
}
pub fn selected(&self) -> bool {
self.flags.contains(SELECTED)
}
} }
/// Describes how to split a fragment. This is used during line breaking as part of the return /// Describes how to split a fragment. This is used during line breaking as part of the return
@ -737,19 +751,17 @@ pub struct UnscannedTextFragmentInfo {
/// The text inside the fragment. /// The text inside the fragment.
pub text: Box<str>, pub text: Box<str>,
/// The position of the insertion point, if any. /// The selected text range. An empty range represents the insertion point.
/// pub selection: Option<Range<CharIndex>>,
/// TODO(pcwalton): Make this a range.
pub insertion_point: Option<CharIndex>,
} }
impl UnscannedTextFragmentInfo { impl UnscannedTextFragmentInfo {
/// Creates a new instance of `UnscannedTextFragmentInfo` from the given text. /// Creates a new instance of `UnscannedTextFragmentInfo` from the given text.
#[inline] #[inline]
pub fn new(text: String, insertion_point: Option<CharIndex>) -> UnscannedTextFragmentInfo { pub fn new(text: String, selection: Option<Range<CharIndex>>) -> UnscannedTextFragmentInfo {
UnscannedTextFragmentInfo { UnscannedTextFragmentInfo {
text: text.into_boxed_str(), text: text.into_boxed_str(),
insertion_point: insertion_point, selection: selection,
} }
} }
} }
@ -865,15 +877,17 @@ impl Fragment {
let size = LogicalSize::new(self.style.writing_mode, let size = LogicalSize::new(self.style.writing_mode,
split.inline_size, split.inline_size,
self.border_box.size.block); self.border_box.size.block);
let requires_line_break_afterward_if_wrapping_on_newlines = let flags = match self.specific {
self.requires_line_break_afterward_if_wrapping_on_newlines(); SpecificFragmentInfo::ScannedText(ref info) => info.flags,
_ => ScannedTextFlags::empty()
};
// FIXME(pcwalton): This should modify the insertion point as necessary. // FIXME(pcwalton): This should modify the insertion point as necessary.
let info = box ScannedTextFragmentInfo::new( let info = box ScannedTextFragmentInfo::new(
text_run, text_run,
split.range, split.range,
size, size,
&None, None,
requires_line_break_afterward_if_wrapping_on_newlines); flags);
self.transform(size, SpecificFragmentInfo::ScannedText(info)) self.transform(size, SpecificFragmentInfo::ScannedText(info))
} }
@ -1681,9 +1695,9 @@ impl Fragment {
this_info.range.extend_to(other_info.range_end_including_stripped_whitespace); this_info.range.extend_to(other_info.range_end_including_stripped_whitespace);
this_info.content_size.inline = this_info.content_size.inline =
this_info.run.metrics_for_range(&this_info.range).advance_width; this_info.run.metrics_for_range(&this_info.range).advance_width;
this_info.requires_line_break_afterward_if_wrapping_on_newlines = if other_info.requires_line_break_afterward_if_wrapping_on_newlines() {
this_info.requires_line_break_afterward_if_wrapping_on_newlines || this_info.flags.insert(REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES);
other_info.requires_line_break_afterward_if_wrapping_on_newlines; }
self.border_padding.inline_end = next_fragment.border_padding.inline_end; self.border_padding.inline_end = next_fragment.border_padding.inline_end;
self.border_box.size.inline = this_info.content_size.inline + self.border_box.size.inline = this_info.content_size.inline +
self.border_padding.inline_start_end(); self.border_padding.inline_start_end();
@ -2247,7 +2261,7 @@ impl Fragment {
pub fn requires_line_break_afterward_if_wrapping_on_newlines(&self) -> bool { pub fn requires_line_break_afterward_if_wrapping_on_newlines(&self) -> bool {
match self.specific { match self.specific {
SpecificFragmentInfo::ScannedText(ref scanned_text) => { SpecificFragmentInfo::ScannedText(ref scanned_text) => {
scanned_text.requires_line_break_afterward_if_wrapping_on_newlines scanned_text.requires_line_break_afterward_if_wrapping_on_newlines()
} }
_ => false, _ => false,
} }

View file

@ -354,6 +354,7 @@ impl LineBreaker {
let need_to_merge = match (&mut result.specific, &candidate.specific) { let need_to_merge = match (&mut result.specific, &candidate.specific) {
(&mut SpecificFragmentInfo::ScannedText(ref mut result_info), (&mut SpecificFragmentInfo::ScannedText(ref mut result_info),
&SpecificFragmentInfo::ScannedText(ref candidate_info)) => { &SpecificFragmentInfo::ScannedText(ref candidate_info)) => {
result_info.selected() == candidate_info.selected() &&
util::arc_ptr_eq(&result_info.run, &candidate_info.run) && util::arc_ptr_eq(&result_info.run, &candidate_info.run) &&
inline_contexts_are_equal(&result.inline_context, inline_contexts_are_equal(&result.inline_context,
&candidate.inline_context) &candidate.inline_context)

View file

@ -7,7 +7,8 @@
#![deny(unsafe_code)] #![deny(unsafe_code)]
use app_units::Au; use app_units::Au;
use fragment::{Fragment, ScannedTextFragmentInfo, SpecificFragmentInfo, UnscannedTextFragmentInfo}; use fragment::{Fragment, REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES, ScannedTextFlags};
use fragment::{ScannedTextFragmentInfo, SELECTED, SpecificFragmentInfo, UnscannedTextFragmentInfo};
use gfx::font::{DISABLE_KERNING_SHAPING_FLAG, FontMetrics, IGNORE_LIGATURES_SHAPING_FLAG}; use gfx::font::{DISABLE_KERNING_SHAPING_FLAG, FontMetrics, IGNORE_LIGATURES_SHAPING_FLAG};
use gfx::font::{RTL_FLAG, RunMetrics, ShapingFlags, ShapingOptions}; use gfx::font::{RTL_FLAG, RunMetrics, ShapingFlags, ShapingOptions};
use gfx::font_context::FontContext; use gfx::font_context::FontContext;
@ -172,17 +173,21 @@ impl TextRunScanner {
for (fragment_index, in_fragment) in self.clump.iter().enumerate() { for (fragment_index, in_fragment) in self.clump.iter().enumerate() {
let mut mapping = RunMapping::new(&run_info_list[..], &run_info, fragment_index); let mut mapping = RunMapping::new(&run_info_list[..], &run_info, fragment_index);
let text; let text;
let insertion_point; let selection;
match in_fragment.specific { match in_fragment.specific {
SpecificFragmentInfo::UnscannedText(ref text_fragment_info) => { SpecificFragmentInfo::UnscannedText(ref text_fragment_info) => {
text = &text_fragment_info.text; text = &text_fragment_info.text;
insertion_point = text_fragment_info.insertion_point; selection = text_fragment_info.selection;
} }
_ => panic!("Expected an unscanned text fragment!"), _ => 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); 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 // Search for the first font in this font group that contains a glyph for this
// character. // character.
let mut font_index = 0; let mut font_index = 0;
@ -213,11 +218,18 @@ impl TextRunScanner {
run_info.script = script; 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. // Now, if necessary, flush the mapping we were building up.
if run_info.font_index != font_index || let flush_run = run_info.font_index != font_index ||
run_info.bidi_level != bidi_level || run_info.bidi_level != bidi_level ||
!compatible_script !compatible_script;
{ let flush_mapping = flush_run || mapping.selected != selected;
if flush_mapping {
if end_position > start_position { if end_position > start_position {
mapping.flush(&mut mappings, mapping.flush(&mut mappings,
&mut run_info, &mut run_info,
@ -230,8 +242,10 @@ impl TextRunScanner {
end_position); end_position);
} }
if run_info.text.len() > 0 { if run_info.text.len() > 0 {
run_info_list.push(run_info); if flush_run {
run_info = RunInfo::new(); run_info_list.push(run_info);
run_info = RunInfo::new();
}
mapping = RunMapping::new(&run_info_list[..], mapping = RunMapping::new(&run_info_list[..],
&run_info, &run_info,
fragment_index); fragment_index);
@ -239,6 +253,7 @@ impl TextRunScanner {
run_info.font_index = font_index; run_info.font_index = font_index;
run_info.bidi_level = bidi_level; run_info.bidi_level = bidi_level;
run_info.script = script; run_info.script = script;
mapping.selected = selected;
} }
// Consume this character. // Consume this character.
@ -330,12 +345,20 @@ impl TextRunScanner {
} }
let text_size = old_fragment.border_box.size; let text_size = old_fragment.border_box.size;
let mut flags = ScannedTextFlags::empty();
if mapping.selected {
flags.insert(SELECTED);
}
if requires_line_break_afterward_if_wrapping_on_newlines {
flags.insert(REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES);
}
let mut new_text_fragment_info = box ScannedTextFragmentInfo::new( let mut new_text_fragment_info = box ScannedTextFragmentInfo::new(
scanned_run.run, scanned_run.run,
mapping.char_range, mapping.char_range,
text_size, text_size,
&scanned_run.insertion_point, scanned_run.insertion_point,
requires_line_break_afterward_if_wrapping_on_newlines); flags);
let new_metrics = new_text_fragment_info.run.metrics_for_range(&mapping.char_range); let new_metrics = new_text_fragment_info.run.metrics_for_range(&mapping.char_range);
let writing_mode = old_fragment.style.writing_mode; let writing_mode = old_fragment.style.writing_mode;
@ -408,7 +431,7 @@ fn split_first_fragment_at_newline_if_necessary(fragments: &mut LinkedList<Fragm
let new_fragment = { let new_fragment = {
let mut first_fragment = fragments.front_mut().unwrap(); let mut first_fragment = fragments.front_mut().unwrap();
let string_before; let string_before;
let insertion_point_before; let selection_before;
{ {
if !first_fragment.white_space().preserve_newlines() { if !first_fragment.white_space().preserve_newlines() {
return; return;
@ -433,21 +456,28 @@ fn split_first_fragment_at_newline_if_necessary(fragments: &mut LinkedList<Fragm
unscanned_text_fragment_info.text = unscanned_text_fragment_info.text =
unscanned_text_fragment_info.text[(position + 1)..].to_owned().into_boxed_str(); unscanned_text_fragment_info.text[(position + 1)..].to_owned().into_boxed_str();
let offset = CharIndex(string_before.char_indices().count() as isize); let offset = CharIndex(string_before.char_indices().count() as isize);
match unscanned_text_fragment_info.insertion_point { match unscanned_text_fragment_info.selection {
Some(insertion_point) if insertion_point >= offset => { Some(ref mut selection) if selection.begin() >= offset => {
insertion_point_before = None; // Selection is entirely in the second fragment.
unscanned_text_fragment_info.insertion_point = Some(insertion_point - offset); selection_before = None;
selection.shift_by(-offset);
} }
Some(_) | None => { Some(ref mut selection) if selection.end() > offset => {
insertion_point_before = unscanned_text_fragment_info.insertion_point; // Selection is split across two fragments.
unscanned_text_fragment_info.insertion_point = None; 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, first_fragment.transform(first_fragment.border_box.size,
SpecificFragmentInfo::UnscannedText( SpecificFragmentInfo::UnscannedText(
UnscannedTextFragmentInfo::new(string_before, UnscannedTextFragmentInfo::new(string_before,
insertion_point_before))) selection_before)))
}; };
fragments.push_front(new_fragment); fragments.push_front(new_fragment);
@ -494,6 +524,8 @@ struct RunMapping {
old_fragment_index: usize, old_fragment_index: usize,
/// The index of the text run we're going to create. /// The index of the text run we're going to create.
text_run_index: usize, text_run_index: usize,
/// Is the text in this fragment selected?
selected: bool,
} }
impl RunMapping { impl RunMapping {
@ -508,6 +540,7 @@ impl RunMapping {
byte_range: Range::new(0, 0), byte_range: Range::new(0, 0),
old_fragment_index: fragment_index, old_fragment_index: fragment_index,
text_run_index: run_info_list.len(), text_run_index: run_info_list.len(),
selected: false,
} }
} }

View file

@ -37,6 +37,7 @@ use gfx::text::glyph::CharIndex;
use incremental::RestyleDamage; use incremental::RestyleDamage;
use msg::constellation_msg::PipelineId; use msg::constellation_msg::PipelineId;
use opaque_node::OpaqueNodeMethods; use opaque_node::OpaqueNodeMethods;
use range::{Range, RangeIndex};
use script::dom::attr::AttrValue; use script::dom::attr::AttrValue;
use script::dom::bindings::inheritance::{Castable, CharacterDataTypeId, ElementTypeId}; use script::dom::bindings::inheritance::{Castable, CharacterDataTypeId, ElementTypeId};
use script::dom::bindings::inheritance::{HTMLElementTypeId, NodeTypeId}; use script::dom::bindings::inheritance::{HTMLElementTypeId, NodeTypeId};
@ -826,7 +827,7 @@ pub trait ThreadSafeLayoutNode : Clone + Copy + Sized + PartialEq {
fn text_content(&self) -> TextContent; fn text_content(&self) -> TextContent;
/// If the insertion point is within this node, returns it. Otherwise, returns `None`. /// If the insertion point is within this node, returns it. Otherwise, returns `None`.
fn insertion_point(&self) -> Option<CharIndex>; fn selection(&self) -> Option<Range<CharIndex>>;
/// If this is an image element, returns its URL. If this is not an image element, fails. /// If this is an image element, returns its URL. If this is not an image element, fails.
/// ///
@ -1050,21 +1051,24 @@ impl<'ln> ThreadSafeLayoutNode for ServoThreadSafeLayoutNode<'ln> {
panic!("not text!") panic!("not text!")
} }
fn insertion_point(&self) -> Option<CharIndex> { fn selection(&self) -> Option<Range<CharIndex>> {
let this = unsafe { let this = unsafe {
self.get_jsmanaged() self.get_jsmanaged()
}; };
if let Some(area) = this.downcast::<HTMLTextAreaElement>() { if let Some(area) = this.downcast::<HTMLTextAreaElement>() {
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() }; 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::<HTMLInputElement>() { if let Some(input) = this.downcast::<HTMLInputElement>() {
let insertion_point_index = unsafe { input.get_insertion_point_index_for_layout() }; if let Some(selection) = unsafe { input.get_selection_for_layout() } {
if let Some(insertion_point_index) = insertion_point_index { return Some(Range::new(CharIndex(selection.begin()),
return Some(CharIndex(insertion_point_index)); CharIndex(selection.length())));
} }
} }
None None

View file

@ -82,6 +82,7 @@ num = "0.1.24"
rand = "0.3" rand = "0.3"
phf = "0.7.13" phf = "0.7.13"
phf_macros = "0.7.13" phf_macros = "0.7.13"
range = { path = "../range" }
ref_filter_map = "1.0" ref_filter_map = "1.0"
ref_slice = "0.1.0" ref_slice = "0.1.0"
regex = "0.1.43" regex = "0.1.43"

View file

@ -31,6 +31,7 @@ use dom::nodelist::NodeList;
use dom::validation::Validatable; use dom::validation::Validatable;
use dom::virtualmethods::VirtualMethods; use dom::virtualmethods::VirtualMethods;
use msg::constellation_msg::ConstellationChan; use msg::constellation_msg::ConstellationChan;
use range::Range;
use script_thread::ScriptThreadEventCategory::InputEvent; use script_thread::ScriptThreadEventCategory::InputEvent;
use script_thread::{CommonScriptMsg, Runnable}; use script_thread::{CommonScriptMsg, Runnable};
use script_traits::ScriptMsg as ConstellationMsg; use script_traits::ScriptMsg as ConstellationMsg;
@ -209,7 +210,7 @@ pub trait LayoutHTMLInputElementHelpers {
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn get_size_for_layout(self) -> u32; unsafe fn get_size_for_layout(self) -> u32;
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn get_insertion_point_index_for_layout(self) -> Option<isize>; unsafe fn get_selection_for_layout(self) -> Option<Range<isize>>;
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn get_checked_state_for_layout(self) -> bool; unsafe fn get_checked_state_for_layout(self) -> bool;
#[allow(unsafe_code)] #[allow(unsafe_code)]
@ -242,7 +243,7 @@ impl LayoutHTMLInputElementHelpers for LayoutJS<HTMLInputElement> {
InputType::InputPassword => { InputType::InputPassword => {
let text = get_raw_textinput_value(self); let text = get_raw_textinput_value(self);
if !text.is_empty() { 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() text.chars().map(|_| '●').collect()
} else { } else {
String::from((*self.unsafe_get()).placeholder.borrow_for_layout().clone()) String::from((*self.unsafe_get()).placeholder.borrow_for_layout().clone())
@ -251,7 +252,7 @@ impl LayoutHTMLInputElementHelpers for LayoutJS<HTMLInputElement> {
_ => { _ => {
let text = get_raw_textinput_value(self); let text = get_raw_textinput_value(self);
if !text.is_empty() { 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) String::from(text)
} else { } else {
String::from((*self.unsafe_get()).placeholder.borrow_for_layout().clone()) String::from((*self.unsafe_get()).placeholder.borrow_for_layout().clone())
@ -268,25 +269,24 @@ impl LayoutHTMLInputElementHelpers for LayoutJS<HTMLInputElement> {
#[allow(unrooted_must_root)] #[allow(unrooted_must_root)]
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn get_insertion_point_index_for_layout(self) -> Option<isize> { unsafe fn get_selection_for_layout(self) -> Option<Range<isize>> {
if !(*self.unsafe_get()).upcast::<Element>().get_focus_state() { if !(*self.unsafe_get()).upcast::<Element>().get_focus_state() {
return None; return None;
} }
match (*self.unsafe_get()).input_type.get() {
InputType::InputText => { // Use the raw textinput to get the index as long as we use a 1:1 char mapping
let raw = self.get_value_for_layout(); // in get_value_for_layout.
Some(search_index((*self.unsafe_get()).textinput.borrow_for_layout().edit_point.index, let raw = match (*self.unsafe_get()).input_type.get() {
raw.char_indices())) InputType::InputText |
} InputType::InputPassword => get_raw_textinput_value(self),
InputType::InputPassword => { _ => return None
// 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 textinput = (*self.unsafe_get()).textinput.borrow_for_layout();
let raw = get_raw_textinput_value(self); let selection = textinput.get_absolute_selection_range();
Some(search_index((*self.unsafe_get()).textinput.borrow_for_layout().edit_point.index, let begin_byte = selection.begin();
raw.char_indices())) let begin = search_index(begin_byte, raw.char_indices());
} let length = search_index(selection.length(), raw[begin_byte..].char_indices());
_ => None Some(Range::new(begin, length))
}
} }
#[allow(unrooted_must_root)] #[allow(unrooted_must_root)]

View file

@ -26,6 +26,7 @@ use dom::nodelist::NodeList;
use dom::validation::Validatable; use dom::validation::Validatable;
use dom::virtualmethods::VirtualMethods; use dom::virtualmethods::VirtualMethods;
use msg::constellation_msg::ConstellationChan; use msg::constellation_msg::ConstellationChan;
use range::Range;
use script_traits::ScriptMsg as ConstellationMsg; use script_traits::ScriptMsg as ConstellationMsg;
use std::cell::Cell; use std::cell::Cell;
use string_cache::Atom; use string_cache::Atom;
@ -46,7 +47,7 @@ pub trait LayoutHTMLTextAreaElementHelpers {
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn get_value_for_layout(self) -> String; unsafe fn get_value_for_layout(self) -> String;
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn get_absolute_insertion_point_for_layout(self) -> Option<usize>; unsafe fn get_absolute_selection_for_layout(self) -> Option<Range<usize>>;
#[allow(unsafe_code)] #[allow(unsafe_code)]
fn get_cols(self) -> u32; fn get_cols(self) -> u32;
#[allow(unsafe_code)] #[allow(unsafe_code)]
@ -62,10 +63,10 @@ impl LayoutHTMLTextAreaElementHelpers for LayoutJS<HTMLTextAreaElement> {
#[allow(unrooted_must_root)] #[allow(unrooted_must_root)]
#[allow(unsafe_code)] #[allow(unsafe_code)]
unsafe fn get_absolute_insertion_point_for_layout(self) -> Option<usize> { unsafe fn get_absolute_selection_for_layout(self) -> Option<Range<usize>> {
if (*self.unsafe_get()).upcast::<Element>().get_focus_state() { if (*self.unsafe_get()).upcast::<Element>().get_focus_state() {
Some((*self.unsafe_get()).textinput.borrow_for_layout() Some((*self.unsafe_get()).textinput.borrow_for_layout()
.get_absolute_insertion_point()) .get_absolute_selection_range())
} else { } else {
None None
} }

View file

@ -58,6 +58,7 @@ extern crate phf;
#[macro_use] #[macro_use]
extern crate profile_traits; extern crate profile_traits;
extern crate rand; extern crate rand;
extern crate range;
extern crate ref_filter_map; extern crate ref_filter_map;
extern crate ref_slice; extern crate ref_slice;
extern crate regex; extern crate regex;

View file

@ -8,6 +8,7 @@ use clipboard_provider::ClipboardProvider;
use dom::keyboardevent::{KeyboardEvent, key_value}; use dom::keyboardevent::{KeyboardEvent, key_value};
use msg::constellation_msg::{ALT, CONTROL, SHIFT, SUPER}; use msg::constellation_msg::{ALT, CONTROL, SHIFT, SUPER};
use msg::constellation_msg::{Key, KeyModifiers}; use msg::constellation_msg::{Key, KeyModifiers};
use range::Range;
use std::borrow::ToOwned; use std::borrow::ToOwned;
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::default::Default; use std::default::Default;
@ -154,6 +155,15 @@ impl<T: ClipboardProvider> TextInput<T> {
}) })
} }
pub fn get_absolute_selection_range(&self) -> Range<usize> {
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<String> { pub fn get_selection_text(&self) -> Option<String> {
self.get_sorted_selection().map(|(begin, end)| { self.get_sorted_selection().map(|(begin, end)| {
if begin.line != end.line { if begin.line != end.line {

View file

@ -1716,6 +1716,7 @@ dependencies = [
"plugins 0.0.1", "plugins 0.0.1",
"profile_traits 0.0.1", "profile_traits 0.0.1",
"rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", "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_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)", "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)", "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)",

1
ports/cef/Cargo.lock generated
View file

@ -1591,6 +1591,7 @@ dependencies = [
"plugins 0.0.1", "plugins 0.0.1",
"profile_traits 0.0.1", "profile_traits 0.0.1",
"rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", "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_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)", "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)", "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)",

1
ports/gonk/Cargo.lock generated
View file

@ -1573,6 +1573,7 @@ dependencies = [
"plugins 0.0.1", "plugins 0.0.1",
"profile_traits 0.0.1", "profile_traits 0.0.1",
"rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", "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_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)", "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)", "regex 0.1.55 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -2540,6 +2540,30 @@
"url": "/_mozilla/css/input_placeholder_ref.html" "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": [ "css/input_whitespace.html": [
{ {
"path": "css/input_whitespace.html", "path": "css/input_whitespace.html",
@ -8842,6 +8866,30 @@
"url": "/_mozilla/css/input_placeholder_ref.html" "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": [ "css/input_whitespace.html": [
{ {
"path": "css/input_whitespace.html", "path": "css/input_whitespace.html",

View file

@ -0,0 +1,28 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>input selection test</title>
<link rel="match" href="input_selection_ref.html">
<style>
input {
font: 16px sans-serif;
border: 0 none;
margin: 0;
padding: 0;
}
::selection {
color: white;
background: rgba(255, 127, 0, 1.0);
}
</style>
</head>
<body>
<input value="Hello">
<script>
var input = document.querySelector("input");
input.focus();
input.setSelectionRange(0, 5);
</script>
</body>
</html>

View file

@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>input selection test</title>
<link rel="match" href="input_selection_ref.html">
<style>
span {
font: 16px sans-serif;
color: white;
background: rgba(255, 128, 0, 1.0);
}
</style>
</head>
<body>
<span>Hello</span>
</body>
</html>