Auto merge of #10361 - mbrubeck:empty-caret, r=pcwalton

Fixes for insertion point caret rendering

This contains several fixes for the code to position and render the insertion point.  The main effect is that the insertion point is now rendered correctly when in an empty input field.  See the individual commit messages for more detais.  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/10361)
<!-- Reviewable:end -->
This commit is contained in:
bors-servo 2016-04-08 06:01:17 +05:30
commit c191dff04e
7 changed files with 132 additions and 37 deletions

View file

@ -1014,13 +1014,9 @@ impl FragmentDisplayListBuilding for Fragment {
// display list items.
let mut clip = (*clip).clone();
self.adjust_clip_for_style(&mut clip, &stacking_relative_border_box);
if !clip.might_intersect_rect(&stacking_relative_border_box) {
return;
}
let empty_rect = !clip.might_intersect_rect(&stacking_relative_border_box);
debug!("Fragment::build_display_list: intersected. Adding display item...");
if self.is_primary_fragment() {
if self.is_primary_fragment() && !empty_rect {
// Add shadows, background, borders, and outlines, if applicable.
if let Some(ref inline_context) = self.inline_context {
for node in inline_context.nodes.iter().rev() {
@ -1080,14 +1076,23 @@ impl FragmentDisplayListBuilding for Fragment {
&stacking_relative_border_box,
&clip);
}
}
// Paint the selection point if necessary.
if self.is_primary_fragment() {
// Paint the selection point if necessary. Even an empty text fragment may have an
// insertion point, so we do this even if `empty_rect` is true.
self.build_display_items_for_selection_if_necessary(state,
&stacking_relative_border_box,
display_list_section,
&clip);
}
if empty_rect {
return;
}
debug!("Fragment::build_display_list: intersected. Adding display item...");
// Create special per-fragment-type display items.
self.build_fragment_type_specific_display_items(state,
&stacking_relative_border_box,

View file

@ -1709,6 +1709,9 @@ impl Fragment {
if other_info.requires_line_break_afterward_if_wrapping_on_newlines() {
this_info.flags.insert(REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES);
}
if other_info.insertion_point.is_some() {
this_info.insertion_point = other_info.insertion_point;
}
self.border_padding.inline_end = next_fragment.border_padding.inline_end;
}
_ => panic!("Can only merge two scanned-text fragments!"),

View file

@ -170,6 +170,8 @@ impl TextRunScanner {
// First, transform/compress text of all the nodes.
let (mut run_info_list, mut run_info) = (Vec::new(), RunInfo::new());
let mut insertion_point = None;
for (fragment_index, in_fragment) in self.clump.iter().enumerate() {
let mut mapping = RunMapping::new(&run_info_list[..], &run_info, fragment_index);
let text;
@ -181,8 +183,12 @@ impl TextRunScanner {
}
_ => panic!("Expected an unscanned text fragment!"),
};
let insertion_point = match selection {
Some(range) if range.is_empty() => Some(range.begin()),
insertion_point = match selection {
Some(range) if range.is_empty() => {
// `range` is the range within the current fragment. To get the range
// within the text run, offset it by the length of the preceding fragments.
Some(range.begin() + CharIndex(run_info.character_length as isize))
}
_ => None
};
@ -230,20 +236,18 @@ impl TextRunScanner {
let flush_mapping = flush_run || mapping.selected != selected;
if flush_mapping {
if end_position > start_position {
mapping.flush(&mut mappings,
&mut run_info,
&**text,
insertion_point,
compression,
text_transform,
&mut last_whitespace,
&mut start_position,
end_position);
}
mapping.flush(&mut mappings,
&mut run_info,
&**text,
insertion_point,
compression,
text_transform,
&mut last_whitespace,
&mut start_position,
end_position);
if run_info.text.len() > 0 {
if flush_run {
run_info_list.push(run_info);
run_info.flush(&mut run_info_list, &mut insertion_point);
run_info = RunInfo::new();
}
mapping = RunMapping::new(&run_info_list[..],
@ -261,11 +265,6 @@ impl TextRunScanner {
*paragraph_bytes_processed += character.len_utf8();
}
// If the mapping is zero-length, don't flush it.
if start_position == end_position {
continue
}
// Flush the last mapping we created for this fragment to the list.
mapping.flush(&mut mappings,
&mut run_info,
@ -279,7 +278,7 @@ impl TextRunScanner {
}
// Push the final run info.
run_info_list.push(run_info);
run_info.flush(&mut run_info_list, &mut insertion_point);
// Per CSS 2.1 § 16.4, "when the resultant space between two characters is not the same
// as the default space, user agents should not use ligatures." This ensures that, for
@ -339,6 +338,7 @@ impl TextRunScanner {
let scanned_run = runs[mapping.text_run_index].clone();
let requires_line_break_afterward_if_wrapping_on_newlines =
!mapping.byte_range.is_empty() &&
scanned_run.run.text.char_at_reverse(mapping.byte_range.end()) == '\n';
if requires_line_break_afterward_if_wrapping_on_newlines {
mapping.char_range.extend_by(CharIndex(-1));
@ -353,11 +353,18 @@ impl TextRunScanner {
if requires_line_break_afterward_if_wrapping_on_newlines {
flags.insert(REQUIRES_LINE_BREAK_AFTERWARD_IF_WRAPPING_ON_NEWLINES);
}
let insertion_point = if mapping.contains_insertion_point(scanned_run.insertion_point) {
scanned_run.insertion_point
} else {
None
};
let mut new_text_fragment_info = box ScannedTextFragmentInfo::new(
scanned_run.run,
mapping.char_range,
text_size,
scanned_run.insertion_point,
insertion_point,
flags);
let new_metrics = new_text_fragment_info.run.metrics_for_range(&mapping.char_range);
@ -511,6 +518,26 @@ impl RunInfo {
script: Script::Common,
}
}
/// Finish processing this RunInfo and add it to the "done" list.
///
/// * `insertion_point`: The position of the insertion point, in characters relative to the start
/// of this text run.
fn flush(mut self,
list: &mut Vec<RunInfo>,
insertion_point: &mut Option<CharIndex>) {
if let Some(idx) = *insertion_point {
let char_len = CharIndex(self.character_length as isize);
if idx <= char_len {
// The insertion point is in this text run.
self.insertion_point = insertion_point.take()
} else {
// Continue looking for the insertion point in the next text run.
*insertion_point = Some(idx - char_len)
}
}
list.push(self);
}
}
/// A mapping from a portion of an unscanned text fragment to the text run we're going to create
@ -557,6 +584,9 @@ impl RunMapping {
last_whitespace: &mut bool,
start_position: &mut usize,
end_position: usize) {
if *start_position == end_position && insertion_point.is_none() {
return;
}
let old_byte_length = run_info.text.len();
*last_whitespace = util::transform_text(&text[(*start_position)..end_position],
compression,
@ -572,18 +602,12 @@ impl RunMapping {
*last_whitespace,
is_first_run);
// Record the position of the insertion point if necessary.
if let Some(insertion_point) = insertion_point {
run_info.insertion_point =
Some(CharIndex(run_info.character_length as isize + insertion_point.0))
}
run_info.character_length = run_info.character_length + character_count;
*start_position = end_position;
// Don't flush empty mappings.
if character_count == 0 {
return
// Don't flush mappings that contain no characters and no insertion_point.
if character_count == 0 && !self.contains_insertion_point(insertion_point) {
return;
}
let new_byte_length = run_info.text.len();
@ -591,6 +615,18 @@ impl RunMapping {
self.char_range.extend_by(CharIndex(character_count as isize));
mappings.push(self)
}
/// Is the insertion point for this text run within this mapping?
///
/// NOTE: We treat the range as inclusive at both ends, since the insertion point can lie
/// before the first character *or* after the last character, and should be drawn even if the
/// text is empty.
fn contains_insertion_point(&self, insertion_point: Option<CharIndex>) -> bool {
match insertion_point {
None => false,
Some(idx) => self.char_range.begin() <= idx && idx <= self.char_range.end()
}
}
}

View file

@ -2579,6 +2579,18 @@
"url": "/_mozilla/css/input_height_a.html"
}
],
"css/input_insertion_point_empty_a.html": [
{
"path": "css/input_insertion_point_empty_a.html",
"references": [
[
"/_mozilla/css/input_insertion_point_empty_ref.html",
"!="
]
],
"url": "/_mozilla/css/input_insertion_point_empty_a.html"
}
],
"css/input_placeholder.html": [
{
"path": "css/input_placeholder.html",
@ -9061,6 +9073,18 @@
"url": "/_mozilla/css/input_height_a.html"
}
],
"css/input_insertion_point_empty_a.html": [
{
"path": "css/input_insertion_point_empty_a.html",
"references": [
[
"/_mozilla/css/input_insertion_point_empty_ref.html",
"!="
]
],
"url": "/_mozilla/css/input_insertion_point_empty_a.html"
}
],
"css/input_placeholder.html": [
{
"path": "css/input_placeholder.html",

View file

@ -11,5 +11,8 @@
<body>
<input id="a">
<input id="b">
<script>
document.getElementById("a").focus();
</script>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>empty input insertion point test</title>
<link rel="mismatch" href="input_insertion_point_empty_ref.html">
</head>
<body>
<input>
<script>
document.querySelector("input").focus();
</script>
</body>
</html>

View file

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>empty input insertion point reference</title>
</head>
<body>
<input>
</body>
</html>