diff options
-rw-r--r-- | components/gfx/font.rs | 11 | ||||
-rw-r--r-- | components/layout/generated_content.rs | 4 | ||||
-rw-r--r-- | components/layout/text.rs | 355 | ||||
-rw-r--r-- | tests/ref/basic.list | 1 | ||||
-rw-r--r-- | tests/ref/line_height_float_placement_a.html | 2 | ||||
-rw-r--r-- | tests/ref/per_glyph_font_fallback_a.html | 24 | ||||
-rw-r--r-- | tests/ref/per_glyph_font_fallback_ref.html | 28 | ||||
-rw-r--r-- | tests/ref/text_align_complex_a.html | 2 | ||||
-rw-r--r-- | tests/ref/text_align_complex_ref.html | 2 |
9 files changed, 308 insertions, 121 deletions
diff --git a/components/gfx/font.rs b/components/gfx/font.rs index 52c3f95c53a..258a1782d28 100644 --- a/components/gfx/font.rs +++ b/components/gfx/font.rs @@ -19,7 +19,7 @@ use platform::font::{FontHandle, FontTable}; use util::geometry::Au; use text::glyph::{GlyphStore, GlyphId}; use text::shaping::ShaperMethods; -use text::{Shaper, TextRun}; +use text::Shaper; use font_template::FontTemplateDescriptor; use platform::font_template::FontTemplateData; @@ -183,6 +183,7 @@ impl Font { return result; } + #[inline] pub fn glyph_index(&self, codepoint: char) -> Option<GlyphId> { let codepoint = match self.variant { font_variant::T::small_caps => codepoint.to_uppercase().next().unwrap(), //FIXME: #5938 @@ -217,14 +218,6 @@ impl FontGroup { fonts: fonts, } } - - pub fn create_textrun(&self, text: String, options: &ShapingOptions) -> TextRun { - assert!(self.fonts.len() > 0); - - // TODO(Issue #177): Actually fall back through the FontGroup when a font is unsuitable. - let mut font_borrow = self.fonts[0].borrow_mut(); - TextRun::new(&mut *font_borrow, text.clone(), options) - } } pub struct RunMetrics { diff --git a/components/layout/generated_content.rs b/components/layout/generated_content.rs index e2ba7dbfe4b..085b86457bb 100644 --- a/components/layout/generated_content.rs +++ b/components/layout/generated_content.rs @@ -427,8 +427,10 @@ fn render_text(layout_context: &LayoutContext, style, incremental::rebuild_and_reflow(), info)); + // FIXME(pcwalton): This should properly handle multiple marker fragments. This could happen + // due to text run splitting. let fragments = TextRunScanner::new().scan_for_runs(layout_context.font_context(), fragments); - debug_assert!(fragments.len() == 1); + debug_assert!(fragments.len() >= 1); fragments.fragments.into_iter().next().unwrap().specific } diff --git a/components/layout/text.rs b/components/layout/text.rs index 9930b389b62..2943fa2556a 100644 --- a/components/layout/text.rs +++ b/components/layout/text.rs @@ -27,7 +27,6 @@ use util::geometry::Au; use util::linked_list::split_off_head; use util::logical_geometry::{LogicalSize, WritingMode}; use util::range::{Range, RangeIndex}; -use util::smallvec::SmallVec1; /// A stack-allocated object for scanning an inline flow into `TextRun`-containing `TextFragment`s. pub struct TextRunScanner { @@ -100,14 +99,9 @@ impl TextRunScanner { } } - // TODO(#177): Text run creation must account for the renderability of text by font group - // fonts. This is probably achieved by creating the font group above and then letting - // `FontGroup` decide which `Font` to stick into the text run. - // // Concatenate all of the transformed strings together, saving the new character indices. - let mut new_ranges: SmallVec1<Range<CharIndex>> = SmallVec1::new(); - let mut char_total = CharIndex(0); - let run = { + let mut mappings: Vec<RunMapping> = Vec::new(); + let runs = { let fontgroup; let compression; let text_transform; @@ -132,39 +126,79 @@ impl TextRunScanner { } // First, transform/compress text of all the nodes. - let mut run_text = String::new(); - for in_fragment in self.clump.iter() { - let in_fragment = match in_fragment.specific { + let (mut run_info_list, mut run_info) = (Vec::new(), RunInfo::new()); + for (fragment_index, in_fragment) in self.clump.iter().enumerate() { + let mut mapping = RunMapping::new(&run_info_list[..], &run_info, fragment_index); + let text = match in_fragment.specific { SpecificFragmentInfo::UnscannedText(ref text_fragment_info) => { &text_fragment_info.text } _ => panic!("Expected an unscanned text fragment!"), }; - let old_length = CharIndex(run_text.chars().count() as isize); - last_whitespace = util::transform_text(&in_fragment, - compression, - last_whitespace, - &mut run_text); - - let added_chars = CharIndex(run_text.chars().count() as isize) - old_length; - new_ranges.push(Range::new(char_total, added_chars)); - char_total = char_total + added_chars; - } + let (mut start_position, mut end_position) = (0, 0); + for character in text.chars() { + // Search for the first font in this font group that contains a glyph for this + // character. + for font_index in 0..fontgroup.fonts.len() { + if font_index < fontgroup.fonts.len() - 1 && + fontgroup.fonts + .get(font_index) + .unwrap() + .borrow() + .glyph_index(character) + .is_none() { + continue + } + + // We found the font we want to use. Now, if necessary, flush the mapping + // we were building up. + if run_info.font_index != font_index { + if run_info.text.len() > 0 { + mapping.flush(&mut mappings, + &mut run_info, + &**text, + compression, + text_transform, + &mut last_whitespace, + &mut start_position, + end_position); + run_info_list.push(run_info); + run_info = RunInfo::new(); + mapping = RunMapping::new(&run_info_list[..], + &run_info, + fragment_index); + } + + run_info.font_index = font_index + } + + + // Consume this character. + end_position += character.len_utf8(); + break + } + } - // Account for `text-transform`. (Confusingly, this is not handled in "text - // transformation" above, but we follow Gecko in the naming.) - self.apply_style_transform_if_necessary(&mut run_text, text_transform); + // If the mapping is zero-length, don't flush it. + if start_position == end_position { + continue + } - // Now create the run. - // - // TextRuns contain a cycle which is usually resolved by the teardown sequence. - // If no clump takes ownership, however, it will leak. - if run_text.len() == 0 { - self.clump = LinkedList::new(); - return last_whitespace + // Flush the last mapping we created for this fragment to the list. + mapping.flush(&mut mappings, + &mut run_info, + &**text, + compression, + text_transform, + &mut last_whitespace, + &mut start_position, + end_position); } + // Push the final run info. + run_info_list.push(run_info); + // 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 // example, `finally` with a wide `letter-spacing` renders as `f i n a l l y` and not @@ -185,94 +219,53 @@ impl TextRunScanner { }; // FIXME(https://github.com/rust-lang/rust/issues/23338) - let mut font = fontgroup.fonts[0].borrow_mut(); - Arc::new(box TextRun::new(&mut *font, run_text, &options)) + run_info_list.into_iter().map(|run_info| { + let mut font = fontgroup.fonts.get(run_info.font_index).unwrap().borrow_mut(); + Arc::new(box TextRun::new(&mut *font, run_info.text, &options)) + }).collect::<Vec<_>>() }; - // Make new fragments with the run and adjusted text indices. + // Make new fragments with the runs and adjusted text indices. debug!("TextRunScanner: pushing {} fragment(s)", self.clump.len()); + let mut mappings = mappings.into_iter().peekable(); for (logical_offset, old_fragment) in mem::replace(&mut self.clump, LinkedList::new()).into_iter().enumerate() { - let mut range = new_ranges[logical_offset]; - if range.is_empty() { - debug!("Elided an `SpecificFragmentInfo::UnscannedText` because it was \ - zero-length after compression"); - continue - } + loop { + match mappings.peek() { + Some(mapping) if mapping.old_fragment_index == logical_offset => {} + Some(_) | None => break, + }; + + let mut mapping = mappings.next().unwrap(); + let run = runs[mapping.text_run_index].clone(); + + let requires_line_break_afterward_if_wrapping_on_newlines = + run.text.char_at_reverse(mapping.range.end().get() as usize) == '\n'; + if requires_line_break_afterward_if_wrapping_on_newlines { + mapping.range.extend_by(CharIndex(-1)) + } - let requires_line_break_afterward_if_wrapping_on_newlines = - run.text.char_at_reverse(range.end().get() as usize) == '\n'; - if requires_line_break_afterward_if_wrapping_on_newlines { - range.extend_by(CharIndex(-1)) + let text_size = old_fragment.border_box.size; + let mut new_text_fragment_info = box ScannedTextFragmentInfo::new( + run, + mapping.range, + text_size, + requires_line_break_afterward_if_wrapping_on_newlines); + + let new_metrics = new_text_fragment_info.run.metrics_for_range(&mapping.range); + let writing_mode = old_fragment.style.writing_mode; + let bounding_box_size = bounding_box_for_run_metrics(&new_metrics, writing_mode); + new_text_fragment_info.content_size = bounding_box_size; + + let new_fragment = old_fragment.transform( + bounding_box_size, + SpecificFragmentInfo::ScannedText(new_text_fragment_info)); + out_fragments.push(new_fragment) } - - let text_size = old_fragment.border_box.size; - let mut new_text_fragment_info = box ScannedTextFragmentInfo::new( - run.clone(), - range, - text_size, - requires_line_break_afterward_if_wrapping_on_newlines); - let new_metrics = new_text_fragment_info.run.metrics_for_range(&range); - let bounding_box_size = bounding_box_for_run_metrics(&new_metrics, - old_fragment.style.writing_mode); - new_text_fragment_info.content_size = bounding_box_size; - let new_fragment = - old_fragment.transform(bounding_box_size, - SpecificFragmentInfo::ScannedText(new_text_fragment_info)); - out_fragments.push(new_fragment) } last_whitespace } - - /// Accounts for `text-transform`. - /// - /// FIXME(#4311, pcwalton): Case mapping can change length of the string; case mapping should - /// be language-specific; `full-width`; use graphemes instead of characters. - fn apply_style_transform_if_necessary(&mut self, - string: &mut String, - text_transform: text_transform::T) { - match text_transform { - text_transform::T::none => {} - text_transform::T::uppercase => { - let length = string.len(); - let original = mem::replace(string, String::with_capacity(length)); - for character in original.chars() { - string.extend(character.to_uppercase()) - } - } - text_transform::T::lowercase => { - let length = string.len(); - let original = mem::replace(string, String::with_capacity(length)); - for character in original.chars() { - string.extend(character.to_lowercase()) - } - } - text_transform::T::capitalize => { - let length = string.len(); - let original = mem::replace(string, String::with_capacity(length)); - let mut capitalize_next_letter = true; - for character in original.chars() { - // FIXME(#4311, pcwalton): Should be the CSS/Unicode notion of a *typographic - // letter unit*, not an *alphabetic* character: - // - // http://dev.w3.org/csswg/css-text/#typographic-letter-unit - if capitalize_next_letter && character.is_alphabetic() { - string.extend(character.to_uppercase()); - capitalize_next_letter = false; - continue - } - - string.push(character); - - // FIXME(#4311, pcwalton): Try UAX29 instead of just whitespace. - if character.is_whitespace() { - capitalize_next_letter = true - } - } - } - } - } } #[inline] @@ -363,3 +356,149 @@ fn split_first_fragment_at_newline_if_necessary(fragments: &mut LinkedList<Fragm fragments.push_front(new_fragment); } +/// Information about a text run that we're about to create. This is used in `scan_for_runs`. +struct RunInfo { + /// The text that will go in this text run. + text: String, + /// The index of the applicable font in the font group. + font_index: usize, + /// A cached copy of the number of Unicode characters in the text run. + character_length: usize, +} + +impl RunInfo { + fn new() -> RunInfo { + RunInfo { + text: String::new(), + font_index: 0, + character_length: 0, + } + } +} + +/// A mapping from a portion of an unscanned text fragment to the text run we're going to create +/// for it. +#[derive(Copy, Clone, Debug)] +struct RunMapping { + /// The range of characters within the text fragment. + range: Range<CharIndex>, + /// The index of the unscanned text fragment that this mapping corresponds to. + old_fragment_index: usize, + /// The index of the text run we're going to create. + text_run_index: usize, +} + +impl RunMapping { + /// Given the current set of text runs, creates a run mapping for the next fragment. + /// `run_info_list` describes the set of runs we've seen already, and `current_run_info` + /// describes the run we just finished processing. + fn new(run_info_list: &[RunInfo], current_run_info: &RunInfo, fragment_index: usize) + -> RunMapping { + RunMapping { + range: Range::new(CharIndex(current_run_info.character_length as isize), CharIndex(0)), + old_fragment_index: fragment_index, + text_run_index: run_info_list.len(), + } + } + + /// Flushes this run mapping to the list. `run_info` describes the text run that we're + /// currently working on. `text` refers to the text of this fragment. + fn flush(mut self, + mappings: &mut Vec<RunMapping>, + run_info: &mut RunInfo, + text: &str, + compression: CompressionMode, + text_transform: text_transform::T, + last_whitespace: &mut bool, + start_position: &mut usize, + end_position: usize) { + let old_byte_length = run_info.text.len(); + *last_whitespace = util::transform_text(&text[(*start_position)..end_position], + compression, + *last_whitespace, + &mut run_info.text); + + // Account for `text-transform`. (Confusingly, this is not handled in "text + // transformation" above, but we follow Gecko in the naming.) + let character_count = apply_style_transform_if_necessary(&mut run_info.text, + old_byte_length, + text_transform); + + run_info.character_length = run_info.character_length + character_count; + *start_position = end_position; + + // Don't flush empty mappings. + if character_count == 0 { + return + } + + self.range.extend_by(CharIndex(character_count as isize)); + mappings.push(self) + } +} + + +/// Accounts for `text-transform`. +/// +/// FIXME(#4311, pcwalton): Case mapping can change length of the string; case mapping should +/// be language-specific; `full-width`; use graphemes instead of characters. +fn apply_style_transform_if_necessary(string: &mut String, + first_character_position: usize, + text_transform: text_transform::T) + -> usize { + match text_transform { + text_transform::T::none => string[first_character_position..].chars().count(), + text_transform::T::uppercase => { + let original = string[first_character_position..].to_owned(); + string.truncate(first_character_position); + let mut count = 0; + for character in original.chars() { + string.push(character.to_uppercase().next().unwrap()); + count += 1; + } + count + } + text_transform::T::lowercase => { + let original = string[first_character_position..].to_owned(); + string.truncate(first_character_position); + let mut count = 0; + for character in original.chars() { + string.push(character.to_lowercase().next().unwrap()); + count += 1; + } + count + } + text_transform::T::capitalize => { + let original = string[first_character_position..].to_owned(); + string.truncate(first_character_position); + + // FIXME(pcwalton): This may not always be correct in the case of something like + // `f<span>oo</span>`. + let mut capitalize_next_letter = true; + let mut count = 0; + for character in original.chars() { + count += 1; + + // FIXME(#4311, pcwalton): Should be the CSS/Unicode notion of a *typographic + // letter unit*, not an *alphabetic* character: + // + // http://dev.w3.org/csswg/css-text/#typographic-letter-unit + if capitalize_next_letter && character.is_alphabetic() { + string.push(character.to_uppercase().next().unwrap()); + capitalize_next_letter = false; + continue + } + + string.push(character); + + // FIXME(#4311, pcwalton): Try UAX29 instead of just whitespace. + if character.is_whitespace() { + capitalize_next_letter = true + } + } + + count + } + } +} + diff --git a/tests/ref/basic.list b/tests/ref/basic.list index d10cc1af2b4..4c3679508da 100644 --- a/tests/ref/basic.list +++ b/tests/ref/basic.list @@ -234,6 +234,7 @@ flaky_cpu == linebreak_simple_a.html linebreak_simple_b.html == overflow_simple_a.html overflow_simple_b.html == overflow_wrap_a.html overflow_wrap_ref.html == overflow_xy_a.html overflow_xy_ref.html +== per_glyph_font_fallback_a.html per_glyph_font_fallback_ref.html == percent_height.html percent_height_ref.html == percentage_height_float_a.html percentage_height_float_ref.html == percentage_height_root.html percentage_height_root_ref.html diff --git a/tests/ref/line_height_float_placement_a.html b/tests/ref/line_height_float_placement_a.html index 79230351e2c..e98032141c7 100644 --- a/tests/ref/line_height_float_placement_a.html +++ b/tests/ref/line_height_float_placement_a.html @@ -9,7 +9,7 @@ } body { font-size: 16px; - font-family: Ahem, monospace; + font-family: Ahem; padding-top: 5px; } #floaty { diff --git a/tests/ref/per_glyph_font_fallback_a.html b/tests/ref/per_glyph_font_fallback_a.html new file mode 100644 index 00000000000..9f4d6c12cd9 --- /dev/null +++ b/tests/ref/per_glyph_font_fallback_a.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that font fallback occurs on a per-glyph basis. --> +<style> +@font-face { + font-family: 'ahem'; + src: url(fonts/ahem/ahem.ttf); +} + +body { + font-family: Ahem, sans-serif; + font-size: 24px; + line-height: 24px; +} +</style> +</head> +<body> +<section>x←</section> +<section>→x</section> +<section>→x←</section> +</body> +</html> + diff --git a/tests/ref/per_glyph_font_fallback_ref.html b/tests/ref/per_glyph_font_fallback_ref.html new file mode 100644 index 00000000000..bb603464846 --- /dev/null +++ b/tests/ref/per_glyph_font_fallback_ref.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that font fallback occurs on a per-glyph basis. --> +<style> +@font-face { + font-family: 'ahem'; + src: url(fonts/ahem/ahem.ttf); +} + +body { + font-family: Ahem, sans-serif; + font-size: 24px; + line-height: 24px; +} + +.arrow { + font-family: sans-serif; +} +</style> +</head> +<body> +<section>x<span class=arrow>←</span></section> +<section><span class=arrow>→</span>x</section> +<section><span class=arrow>→</span>x<span class=arrow>←</span></section> +</body> +</html> + diff --git a/tests/ref/text_align_complex_a.html b/tests/ref/text_align_complex_a.html index 24b20a072c5..3a80d4e8c60 100644 --- a/tests/ref/text_align_complex_a.html +++ b/tests/ref/text_align_complex_a.html @@ -9,7 +9,7 @@ div {
width: 100px;
font-size: 10px;
- font-family: Ahem, monospace;
+ font-family: Ahem;
padding: 10px !important;
}
diff --git a/tests/ref/text_align_complex_ref.html b/tests/ref/text_align_complex_ref.html index b716c0265bc..4526c34d65e 100644 --- a/tests/ref/text_align_complex_ref.html +++ b/tests/ref/text_align_complex_ref.html @@ -9,7 +9,7 @@ div { width: 100px; font-size: 10px; - font-family: Ahem, monospace; + font-family: Ahem; padding: 10px !important; } |