aboutsummaryrefslogtreecommitdiffstats
path: root/components/layout/text.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/layout/text.rs')
-rw-r--r--components/layout/text.rs327
1 files changed, 327 insertions, 0 deletions
diff --git a/components/layout/text.rs b/components/layout/text.rs
new file mode 100644
index 00000000000..e90272e218a
--- /dev/null
+++ b/components/layout/text.rs
@@ -0,0 +1,327 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! Text layout.
+
+#![deny(unsafe_block)]
+
+use flow::Flow;
+use fragment::{Fragment, ScannedTextFragment, ScannedTextFragmentInfo, UnscannedTextFragment};
+
+use gfx::font::{FontMetrics, FontStyle, RunMetrics};
+use gfx::font_context::FontContext;
+use gfx::text::glyph::CharIndex;
+use gfx::text::text_run::TextRun;
+use gfx::text::util::{CompressWhitespaceNewline, transform_text, CompressNone};
+use servo_util::geometry::Au;
+use servo_util::logical_geometry::{LogicalSize, WritingMode};
+use servo_util::range::Range;
+use style::ComputedValues;
+use style::computed_values::{font_family, line_height, text_orientation, white_space};
+use sync::Arc;
+
+struct NewLinePositions {
+ new_line_pos: Vec<CharIndex>,
+}
+
+// A helper function.
+fn can_coalesce_text_nodes(fragments: &[Fragment], left_i: uint, right_i: uint) -> bool {
+ assert!(left_i != right_i);
+ fragments[left_i].can_merge_with_fragment(&fragments[right_i])
+}
+
+/// A stack-allocated object for scanning an inline flow into `TextRun`-containing `TextFragment`s.
+pub struct TextRunScanner {
+ pub clump: Range<CharIndex>,
+}
+
+impl TextRunScanner {
+ pub fn new() -> TextRunScanner {
+ TextRunScanner {
+ clump: Range::empty(),
+ }
+ }
+
+ pub fn scan_for_runs(&mut self, font_context: &mut FontContext, flow: &mut Flow) {
+ {
+ let inline = flow.as_immutable_inline();
+ debug!("TextRunScanner: scanning {:u} fragments for text runs...", inline.fragments.len());
+ }
+
+ let fragments = &mut flow.as_inline().fragments;
+
+ let mut last_whitespace = true;
+ let mut new_fragments = Vec::new();
+ for fragment_i in range(0, fragments.fragments.len()) {
+ debug!("TextRunScanner: considering fragment: {:u}", fragment_i);
+ if fragment_i > 0 && !can_coalesce_text_nodes(fragments.fragments.as_slice(), fragment_i - 1, fragment_i) {
+ last_whitespace = self.flush_clump_to_list(font_context,
+ fragments.fragments.as_slice(),
+ &mut new_fragments,
+ last_whitespace);
+ }
+
+ self.clump.extend_by(CharIndex(1));
+ }
+
+ // Handle remaining clumps.
+ if self.clump.length() > CharIndex(0) {
+ drop(self.flush_clump_to_list(font_context,
+ fragments.fragments.as_slice(),
+ &mut new_fragments,
+ last_whitespace))
+ }
+
+ debug!("TextRunScanner: swapping out fragments.");
+
+ fragments.fragments = new_fragments;
+ }
+
+ /// A "clump" is a range of inline flow leaves that can be merged together into a single
+ /// fragment. Adjacent text with the same style can be merged, and nothing else can.
+ ///
+ /// The flow keeps track of the fragments contained by all non-leaf DOM nodes. This is necessary
+ /// for correct painting order. Since we compress several leaf fragments here, the mapping must
+ /// be adjusted.
+ ///
+ /// FIXME(#2267, pcwalton): Stop cloning fragments. Instead we will need to replace each
+ /// `in_fragment` with some smaller stub.
+ fn flush_clump_to_list(&mut self,
+ font_context: &mut FontContext,
+ in_fragments: &[Fragment],
+ out_fragments: &mut Vec<Fragment>,
+ last_whitespace: bool)
+ -> bool {
+ assert!(self.clump.length() > CharIndex(0));
+
+ debug!("TextRunScanner: flushing fragments in range={}", self.clump);
+ let is_singleton = self.clump.length() == CharIndex(1);
+
+ let is_text_clump = match in_fragments[self.clump.begin().to_uint()].specific {
+ UnscannedTextFragment(_) => true,
+ _ => false,
+ };
+
+ let mut new_whitespace = last_whitespace;
+ match (is_singleton, is_text_clump) {
+ (false, false) => {
+ fail!("WAT: can't coalesce non-text nodes in flush_clump_to_list()!")
+ }
+ (true, false) => {
+ // FIXME(pcwalton): Stop cloning fragments, as above.
+ debug!("TextRunScanner: pushing single non-text fragment in range: {}", self.clump);
+ let new_fragment = in_fragments[self.clump.begin().to_uint()].clone();
+ out_fragments.push(new_fragment)
+ },
+ (true, true) => {
+ let old_fragment = &in_fragments[self.clump.begin().to_uint()];
+ let text = match old_fragment.specific {
+ UnscannedTextFragment(ref text_fragment_info) => &text_fragment_info.text,
+ _ => fail!("Expected an unscanned text fragment!"),
+ };
+
+ let font_style = old_fragment.font_style();
+
+ let compression = match old_fragment.white_space() {
+ white_space::normal => CompressWhitespaceNewline,
+ white_space::pre => CompressNone,
+ };
+
+ let mut new_line_pos = vec![];
+
+ let (transformed_text, whitespace) = transform_text(text.as_slice(),
+ compression,
+ last_whitespace,
+ &mut new_line_pos);
+
+ new_whitespace = whitespace;
+
+ if transformed_text.len() > 0 {
+ // 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.
+ let fontgroup = font_context.get_layout_font_group_for_style(&font_style);
+ let run = box fontgroup.create_textrun(
+ transformed_text.clone());
+
+ debug!("TextRunScanner: pushing single text fragment in range: {} ({})",
+ self.clump,
+ *text);
+ let range = Range::new(CharIndex(0), run.char_len());
+ let new_metrics = run.metrics_for_range(&range);
+ let bounding_box_size = bounding_box_for_run_metrics(
+ &new_metrics, old_fragment.style.writing_mode);
+ let new_text_fragment_info = ScannedTextFragmentInfo::new(Arc::new(run), range);
+ let mut new_fragment = old_fragment.transform(
+ bounding_box_size, ScannedTextFragment(new_text_fragment_info));
+ new_fragment.new_line_pos = new_line_pos;
+ out_fragments.push(new_fragment)
+ }
+ },
+ (false, true) => {
+ // 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.
+ let in_fragment = &in_fragments[self.clump.begin().to_uint()];
+ let font_style = in_fragment.font_style();
+ let fontgroup = font_context.get_layout_font_group_for_style(&font_style);
+
+ let compression = match in_fragment.white_space() {
+ white_space::normal => CompressWhitespaceNewline,
+ white_space::pre => CompressNone,
+ };
+
+ let mut new_line_positions: Vec<NewLinePositions> = vec![];
+
+ // First, transform/compress text of all the nodes.
+ let mut last_whitespace_in_clump = new_whitespace;
+ let transformed_strs: Vec<String> = Vec::from_fn(self.clump.length().to_uint(), |i| {
+ let idx = CharIndex(i as int) + self.clump.begin();
+ let in_fragment = match in_fragments[idx.to_uint()].specific {
+ UnscannedTextFragment(ref text_fragment_info) => &text_fragment_info.text,
+ _ => fail!("Expected an unscanned text fragment!"),
+ };
+
+ let mut new_line_pos = vec![];
+
+ let (new_str, new_whitespace) = transform_text(in_fragment.as_slice(),
+ compression,
+ last_whitespace_in_clump,
+ &mut new_line_pos);
+ new_line_positions.push(NewLinePositions { new_line_pos: new_line_pos });
+
+ last_whitespace_in_clump = new_whitespace;
+ new_str
+ });
+ new_whitespace = last_whitespace_in_clump;
+
+ // Next, concatenate all of the transformed strings together, saving the new
+ // character indices.
+ let mut run_str = String::new();
+ let mut new_ranges: Vec<Range<CharIndex>> = vec![];
+ let mut char_total = CharIndex(0);
+ for i in range(0, transformed_strs.len() as int) {
+ let added_chars = CharIndex(transformed_strs[i as uint].as_slice().char_len() as int);
+ new_ranges.push(Range::new(char_total, added_chars));
+ run_str.push_str(transformed_strs[i as uint].as_slice());
+ char_total = char_total + added_chars;
+ }
+
+ // 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.
+ let clump = self.clump;
+ let run = if clump.length() != CharIndex(0) && run_str.len() > 0 {
+ Some(Arc::new(box TextRun::new(
+ &mut *fontgroup.fonts[0].borrow_mut(),
+ run_str.to_string())))
+ } else {
+ None
+ };
+
+ // Make new fragments with the run and adjusted text indices.
+ debug!("TextRunScanner: pushing fragment(s) in range: {}", self.clump);
+ for i in clump.each_index() {
+ let logical_offset = i - self.clump.begin();
+ let range = new_ranges[logical_offset.to_uint()];
+ if range.length() == CharIndex(0) {
+ debug!("Elided an `UnscannedTextFragment` because it was zero-length after \
+ compression; {}", in_fragments[i.to_uint()]);
+ continue
+ }
+
+ let new_text_fragment_info = ScannedTextFragmentInfo::new(run.get_ref().clone(), range);
+ let old_fragment = &in_fragments[i.to_uint()];
+ 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);
+ let mut new_fragment = old_fragment.transform(
+ bounding_box_size, ScannedTextFragment(new_text_fragment_info));
+ new_fragment.new_line_pos = new_line_positions[logical_offset.to_uint()].new_line_pos.clone();
+ out_fragments.push(new_fragment)
+ }
+ }
+ } // End of match.
+
+ let end = self.clump.end(); // FIXME: borrow checker workaround
+ self.clump.reset(end, CharIndex(0));
+
+ new_whitespace
+ } // End of `flush_clump_to_list`.
+}
+
+
+#[inline]
+fn bounding_box_for_run_metrics(metrics: &RunMetrics, writing_mode: WritingMode)
+ -> LogicalSize<Au> {
+
+ // This does nothing, but it will fail to build
+ // when more values are added to the `text-orientation` CSS property.
+ // This will be a reminder to update the code below.
+ let dummy: Option<text_orientation::T> = None;
+ match dummy {
+ Some(text_orientation::sideways_right) |
+ Some(text_orientation::sideways_left) |
+ Some(text_orientation::sideways) |
+ None => {}
+ }
+
+ // In vertical sideways or horizontal upgright text,
+ // the "width" of text metrics is always inline
+ // This will need to be updated when other text orientations are supported.
+ LogicalSize::new(
+ writing_mode,
+ metrics.bounding_box.size.width,
+ metrics.bounding_box.size.height)
+
+}
+
+/// Returns the metrics of the font represented by the given `FontStyle`, respectively.
+///
+/// `#[inline]` because often the caller only needs a few fields from the font metrics.
+#[inline]
+pub fn font_metrics_for_style(font_context: &mut FontContext, font_style: &FontStyle)
+ -> FontMetrics {
+ let fontgroup = font_context.get_layout_font_group_for_style(font_style);
+ fontgroup.fonts[0].borrow().metrics.clone()
+}
+
+/// Converts a computed style to a font style used for rendering.
+///
+/// FIXME(pcwalton): This should not be necessary; just make the font part of the style sharable
+/// with the display list somehow. (Perhaps we should use an ARC.)
+pub fn computed_style_to_font_style(style: &ComputedValues) -> FontStyle {
+ debug!("(font style) start");
+
+ // FIXME: Too much allocation here.
+ let mut font_families = style.get_font().font_family.iter().map(|family| {
+ match *family {
+ font_family::FamilyName(ref name) => (*name).clone(),
+ }
+ });
+ debug!("(font style) font families: `{:?}`", font_families);
+
+ let font_size = style.get_font().font_size.to_f64().unwrap() / 60.0;
+ debug!("(font style) font size: `{:f}px`", font_size);
+
+ FontStyle {
+ pt_size: font_size,
+ weight: style.get_font().font_weight,
+ style: style.get_font().font_style,
+ families: font_families.collect(),
+ }
+}
+
+/// Returns the line block-size needed by the given computed style and font size.
+pub fn line_height_from_style(style: &ComputedValues, metrics: &FontMetrics) -> Au {
+ let font_size = style.get_font().font_size;
+ let from_inline = match style.get_inheritedbox().line_height {
+ line_height::Normal => metrics.line_gap,
+ line_height::Number(l) => font_size.scale_by(l),
+ line_height::Length(l) => l
+ };
+ let minimum = style.get_inheritedbox()._servo_minimum_line_height;
+ Au::max(from_inline, minimum)
+}
+