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
}
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(),

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.
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);

View file

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

View file

@ -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)

View file

@ -7,7 +7,8 @@
#![deny(unsafe_code)]
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::{RTL_FLAG, RunMetrics, ShapingFlags, ShapingOptions};
use gfx::font_context::FontContext;
@ -172,17 +173,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 +218,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 +242,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 +253,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.
@ -330,12 +345,20 @@ impl TextRunScanner {
}
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(
scanned_run.run,
mapping.char_range,
text_size,
&scanned_run.insertion_point,
requires_line_break_afterward_if_wrapping_on_newlines);
scanned_run.insertion_point,
flags);
let new_metrics = new_text_fragment_info.run.metrics_for_range(&mapping.char_range);
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 mut first_fragment = fragments.front_mut().unwrap();
let string_before;
let insertion_point_before;
let selection_before;
{
if !first_fragment.white_space().preserve_newlines() {
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[(position + 1)..].to_owned().into_boxed_str();
let offset = CharIndex(string_before.char_indices().count() as isize);
match unscanned_text_fragment_info.insertion_point {
Some(insertion_point) if insertion_point >= 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 +524,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 +540,7 @@ impl RunMapping {
byte_range: Range::new(0, 0),
old_fragment_index: fragment_index,
text_run_index: run_info_list.len(),
selected: false,
}
}

View file

@ -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};
@ -826,7 +827,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<CharIndex>;
fn selection(&self) -> Option<Range<CharIndex>>;
/// 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!")
}
fn insertion_point(&self) -> Option<CharIndex> {
fn selection(&self) -> Option<Range<CharIndex>> {
let this = unsafe {
self.get_jsmanaged()
};
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() };
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>() {
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