diff options
-rw-r--r-- | components/gfx/text/glyph.rs | 75 | ||||
-rw-r--r-- | components/gfx/text/shaping/harfbuzz.rs | 2 | ||||
-rw-r--r-- | components/gfx/text/text_run.rs | 4 | ||||
-rw-r--r-- | components/layout/fragment.rs | 147 | ||||
-rw-r--r-- | components/layout/inline.rs | 150 | ||||
-rw-r--r-- | components/script/dom/webidls/CSSStyleDeclaration.webidl | 1 | ||||
-rw-r--r-- | components/style/properties/mod.rs.mako | 3 | ||||
-rw-r--r-- | tests/ref/basic.list | 4 | ||||
-rw-r--r-- | tests/ref/outlines_wrap_ref.html | 2 | ||||
-rw-r--r-- | tests/ref/text_align_complex_a.html | 39 | ||||
-rw-r--r-- | tests/ref/text_align_complex_ref.html | 42 | ||||
-rw-r--r-- | tests/ref/text_align_justify_a.html | 22 | ||||
-rw-r--r-- | tests/ref/text_align_justify_ref.html | 41 | ||||
-rw-r--r-- | tests/ref/text_justify_none_a.html | 15 | ||||
-rw-r--r-- | tests/ref/text_justify_none_ref.html | 14 | ||||
-rw-r--r-- | tests/ref/text_overflow_basic_a.html | 15 | ||||
-rw-r--r-- | tests/ref/text_overflow_basic_ref.html | 15 |
17 files changed, 528 insertions, 63 deletions
diff --git a/components/gfx/text/glyph.rs b/components/gfx/text/glyph.rs index 39aab3c068d..76a0c73e34b 100644 --- a/components/gfx/text/glyph.rs +++ b/components/gfx/text/glyph.rs @@ -2,19 +2,17 @@ * 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/. */ -use util::vec::*; -use util::range; -use util::range::{Range, RangeIndex, EachIndex}; -use util::geometry::Au; - +use geom::point::Point2D; use std::cmp::{Ordering, PartialOrd}; use std::iter::repeat; -use std::num::{ToPrimitive, NumCast}; use std::mem; +use std::num::{ToPrimitive, NumCast}; use std::ops::{Add, Sub, Mul, Neg, Div, Rem, BitAnd, BitOr, BitXor, Shl, Shr, Not}; use std::u16; use std::vec::Vec; -use geom::point::Point2D; +use util::geometry::Au; +use util::range::{mod, Range, RangeIndex, EachIndex}; +use util::vec::*; /// GlyphEntry is a port of Gecko's CompressedGlyph scheme for storing glyph data compactly. /// @@ -256,7 +254,7 @@ impl GlyphEntry { #[derive(Clone, Show, Copy)] struct DetailedGlyph { id: GlyphId, - // glyph's advance, in the text's direction (RTL or RTL) + // glyph's advance, in the text's direction (LTR or RTL) advance: Au, // glyph's offset from the font's em-box (from top-left) offset: Point2D<Au>, @@ -296,6 +294,7 @@ impl Ord for DetailedGlyphRecord { // until a lookup is actually performed; this matches the expected // usage pattern of setting/appending all the detailed glyphs, and // then querying without setting. +#[derive(Clone)] struct DetailedGlyphStore { // TODO(pcwalton): Allocation of this buffer is expensive. Consider a small-vector // optimization. @@ -424,6 +423,7 @@ pub struct GlyphData { } impl GlyphData { + /// Creates a new entry for one glyph. pub fn new(id: GlyphId, advance: Au, offset: Option<Point2D<Au>>, @@ -501,6 +501,7 @@ impl<'a> GlyphInfo<'a> { /// | +---+---+ | /// +---------------------------------------------+ /// ~~~ +#[derive(Clone)] pub struct GlyphStore { // TODO(pcwalton): Allocation of this buffer is expensive. Consider a small-vector // optimization. @@ -547,7 +548,12 @@ impl<'a> GlyphStore { self.detail_store.ensure_sorted(); } - pub fn add_glyph_for_char_index(&mut self, i: CharIndex, data: &GlyphData) { + /// Adds a single glyph. If `character` is present, this represents a single character; + /// otherwise, this glyph represents multiple characters. + pub fn add_glyph_for_char_index(&mut self, + i: CharIndex, + character: Option<char>, + data: &GlyphData) { fn glyph_is_compressible(data: &GlyphData) -> bool { is_simple_glyph_id(data.id) && is_simple_advance(data.advance) @@ -555,10 +561,10 @@ impl<'a> GlyphStore { && data.cluster_start // others are stored in detail buffer } - assert!(data.ligature_start); // can't compress ligature continuation glyphs. - assert!(i < self.char_len()); + debug_assert!(data.ligature_start); // can't compress ligature continuation glyphs. + debug_assert!(i < self.char_len()); - let entry = match (data.is_missing, glyph_is_compressible(data)) { + let mut entry = match (data.is_missing, glyph_is_compressible(data)) { (true, _) => GlyphEntry::missing(1), (false, true) => GlyphEntry::simple(data.id, data.advance), (false, false) => { @@ -566,7 +572,14 @@ impl<'a> GlyphStore { self.detail_store.add_detailed_glyphs_for_entry(i, glyph); GlyphEntry::complex(data.cluster_start, data.ligature_start, 1) } - }.adapt_character_flags_of_entry(self.entry_buffer[i.to_uint()]); + }; + + // FIXME(pcwalton): Is this necessary? I think it's a no-op. + entry = entry.adapt_character_flags_of_entry(self.entry_buffer[i.to_uint()]); + + if character == Some(' ') { + entry = entry.set_char_is_space() + } self.entry_buffer[i.to_uint()] = entry; } @@ -691,13 +704,43 @@ impl<'a> GlyphStore { let entry = self.entry_buffer[i.to_uint()]; self.entry_buffer[i.to_uint()] = entry.set_can_break_before(t); } + + pub fn space_count_in_range(&self, range: &Range<CharIndex>) -> u32 { + let mut spaces = 0; + for index in range.each_index() { + if self.char_is_space(index) { + spaces += 1 + } + } + spaces + } + + pub fn distribute_extra_space_in_range(&mut self, range: &Range<CharIndex>, space: f64) { + debug_assert!(space >= 0.0); + if range.is_empty() { + return + } + for index in range.each_index() { + // TODO(pcwalton): Handle spaces that are detailed glyphs -- these are uncommon but + // possible. + let mut entry = &mut self.entry_buffer[index.to_uint()]; + if entry.is_simple() && entry.char_is_space() { + // FIXME(pcwalton): This can overflow for very large font-sizes. + let advance = + ((entry.value & GLYPH_ADVANCE_MASK) >> (GLYPH_ADVANCE_SHIFT as uint)) + + Au::from_frac_px(space).to_u32().unwrap(); + entry.value = (entry.value & !GLYPH_ADVANCE_MASK) | + (advance << (GLYPH_ADVANCE_SHIFT as uint)); + } + } + } } /// An iterator over the glyphs in a character range in a `GlyphStore`. pub struct GlyphIterator<'a> { - store: &'a GlyphStore, - char_index: CharIndex, - char_range: EachIndex<int, CharIndex>, + store: &'a GlyphStore, + char_index: CharIndex, + char_range: EachIndex<int, CharIndex>, glyph_range: Option<EachIndex<int, CharIndex>>, } diff --git a/components/gfx/text/shaping/harfbuzz.rs b/components/gfx/text/shaping/harfbuzz.rs index cd99abf89b0..5b12ba22b74 100644 --- a/components/gfx/text/shaping/harfbuzz.rs +++ b/components/gfx/text/shaping/harfbuzz.rs @@ -464,7 +464,7 @@ impl Shaper { false, true, true); - glyphs.add_glyph_for_char_index(char_idx, &data); + glyphs.add_glyph_for_char_index(char_idx, Some(character), &data); } else { // collect all glyphs to be assigned to the first character. let mut datas = vec!(); diff --git a/components/gfx/text/text_run.rs b/components/gfx/text/text_run.rs index b2b0d4661c9..20429079c7e 100644 --- a/components/gfx/text/text_run.rs +++ b/components/gfx/text/text_run.rs @@ -120,7 +120,7 @@ impl<'a> Iterator for CharacterSliceIterator<'a> { debug_assert!(!self.range.is_empty()); let index_to_return = self.range.begin(); - self.range.adjust_by(CharIndex(1), CharIndex(0)); + self.range.adjust_by(CharIndex(1), CharIndex(-1)); if self.range.is_empty() { // We're done. self.glyph_run = None @@ -297,7 +297,7 @@ impl<'a> TextRun { pub fn advance_for_range(&self, range: &Range<CharIndex>) -> Au { // TODO(Issue #199): alter advance direction for RTL - // TODO(Issue #98): using inter-char and inter-word spacing settings when measuring text + // TODO(Issue #98): using inter-char and inter-word spacing settings when measuring text self.natural_word_slices_in_range(range) .fold(Au(0), |advance, slice| { advance + slice.glyphs.advance_for_char_range(&slice.range) diff --git a/components/layout/fragment.rs b/components/layout/fragment.rs index 281eeaaa5d2..2b802c43cdd 100644 --- a/components/layout/fragment.rs +++ b/components/layout/fragment.rs @@ -598,9 +598,10 @@ pub struct SplitInfo { impl SplitInfo { fn new(range: Range<CharIndex>, info: &ScannedTextFragmentInfo) -> SplitInfo { + let inline_size = info.run.advance_for_range(&range); SplitInfo { range: range, - inline_size: info.run.advance_for_range(&range), + inline_size: inline_size, } } } @@ -1169,7 +1170,9 @@ impl Fragment { pub fn inline_start_offset(&self) -> Au { match self.specific { SpecificFragmentInfo::TableWrapper => self.margin.inline_start, - SpecificFragmentInfo::Table | SpecificFragmentInfo::TableCell | SpecificFragmentInfo::TableRow => self.border_padding.inline_start, + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow => self.border_padding.inline_start, SpecificFragmentInfo::TableColumn(_) => Au(0), _ => self.margin.inline_start + self.border_padding.inline_start, } @@ -1208,9 +1211,12 @@ impl Fragment { pub fn compute_intrinsic_inline_sizes(&mut self) -> IntrinsicISizesContribution { let mut result = self.style_specified_intrinsic_inline_size(); match self.specific { - SpecificFragmentInfo::Generic | SpecificFragmentInfo::Iframe(_) | - SpecificFragmentInfo::Table | SpecificFragmentInfo::TableCell | - SpecificFragmentInfo::TableColumn(_) | SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableColumn(_) | + SpecificFragmentInfo::TableRow | SpecificFragmentInfo::TableWrapper | SpecificFragmentInfo::InlineAbsoluteHypothetical(_) => {} SpecificFragmentInfo::InlineBlock(ref mut info) => { @@ -1274,8 +1280,13 @@ impl Fragment { /// TODO: What exactly does this function return? Why is it Au(0) for SpecificFragmentInfo::Generic? pub fn content_inline_size(&self) -> Au { match self.specific { - SpecificFragmentInfo::Generic | SpecificFragmentInfo::Iframe(_) | SpecificFragmentInfo::Table | SpecificFragmentInfo::TableCell | - SpecificFragmentInfo::TableRow | SpecificFragmentInfo::TableWrapper | SpecificFragmentInfo::InlineBlock(_) | + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper | + SpecificFragmentInfo::InlineBlock(_) | SpecificFragmentInfo::InlineAbsoluteHypothetical(_) => Au(0), SpecificFragmentInfo::Canvas(ref canvas_fragment_info) => { canvas_fragment_info.replaced_image_fragment_info.computed_inline_size() @@ -1288,16 +1299,25 @@ impl Fragment { let text_bounds = run.metrics_for_range(range).bounding_box; text_bounds.size.width } - SpecificFragmentInfo::TableColumn(_) => panic!("Table column fragments do not have inline_size"), - SpecificFragmentInfo::UnscannedText(_) => panic!("Unscanned text fragments should have been scanned by now!"), + SpecificFragmentInfo::TableColumn(_) => { + panic!("Table column fragments do not have inline_size") + } + SpecificFragmentInfo::UnscannedText(_) => { + panic!("Unscanned text fragments should have been scanned by now!") + } } } /// Returns, and computes, the block-size of this fragment. pub fn content_block_size(&self, layout_context: &LayoutContext) -> Au { match self.specific { - SpecificFragmentInfo::Generic | SpecificFragmentInfo::Iframe(_) | SpecificFragmentInfo::Table | SpecificFragmentInfo::TableCell | - SpecificFragmentInfo::TableRow | SpecificFragmentInfo::TableWrapper | SpecificFragmentInfo::InlineBlock(_) | + SpecificFragmentInfo::Generic | + SpecificFragmentInfo::Iframe(_) | + SpecificFragmentInfo::Table | + SpecificFragmentInfo::TableCell | + SpecificFragmentInfo::TableRow | + SpecificFragmentInfo::TableWrapper | + SpecificFragmentInfo::InlineBlock(_) | SpecificFragmentInfo::InlineAbsoluteHypothetical(_) => Au(0), SpecificFragmentInfo::Image(ref image_fragment_info) => { image_fragment_info.replaced_image_fragment_info.computed_block_size() @@ -1309,8 +1329,12 @@ impl Fragment { // Compute the block-size based on the line-block-size and font size. self.calculate_line_height(layout_context) } - SpecificFragmentInfo::TableColumn(_) => panic!("Table column fragments do not have block_size"), - SpecificFragmentInfo::UnscannedText(_) => panic!("Unscanned text fragments should have been scanned by now!"), + SpecificFragmentInfo::TableColumn(_) => { + panic!("Table column fragments do not have block_size") + } + SpecificFragmentInfo::UnscannedText(_) => { + panic!("Unscanned text fragments should have been scanned by now!") + } } } @@ -1463,7 +1487,8 @@ impl Fragment { max_inline_size: Au, flags: SplitOptions) -> Option<SplitResult> - where I: Iterator<Item=TextRunSlice<'a>> { + where I: Iterator<Item= + TextRunSlice<'a>> { let text_fragment_info = if let SpecificFragmentInfo::ScannedText(ref text_fragment_info) = self.specific { text_fragment_info @@ -1475,6 +1500,7 @@ impl Fragment { let mut remaining_inline_size = max_inline_size; let mut inline_start_range = Range::new(text_fragment_info.range.begin(), CharIndex(0)); let mut inline_end_range = None; + let mut overflowing = false; debug!("calculate_split_position: splitting text fragment (strlen={}, range={:?}, \ max_inline_size={:?})", @@ -1508,12 +1534,23 @@ impl Fragment { continue } - // The advance is more than the remaining inline-size, so split here. - let slice_begin = slice.text_run_range().begin(); + // The advance is more than the remaining inline-size, so split here. First, check to + // see if we're going to overflow the line. If so, perform a best-effort split. + let mut remaining_range = slice.text_run_range(); + if inline_start_range.is_empty() { + // We're going to overflow the line. + overflowing = true; + inline_start_range = slice.text_run_range(); + remaining_range = Range::new(slice.text_run_range().end(), CharIndex(0)); + remaining_range.extend_to(text_fragment_info.range.end()); + } + + // Check to see if we need to create an inline-end chunk. + let slice_begin = remaining_range.begin(); if slice_begin < text_fragment_info.range.end() { // There still some things left over at the end of the line, so create the // inline-end chunk. - let mut inline_end = slice.text_run_range(); + let mut inline_end = remaining_range; inline_end.extend_to(text_fragment_info.range.end()); inline_end_range = Some(inline_end); debug!("calculate_split_position: splitting remainder with inline-end range={:?}", @@ -1525,8 +1562,7 @@ impl Fragment { } // If we failed to find a suitable split point, we're on the verge of overflowing the line. - let inline_start_is_some = inline_start_range.length() > CharIndex(0); - if pieces_processed_count == 1 || !inline_start_is_some { + if inline_start_range.is_empty() || overflowing { // If we've been instructed to retry at character boundaries (probably via // `overflow-wrap: break-word`), do so. if flags.contains(RETRY_AT_CHARACTER_BOUNDARIES) { @@ -1547,7 +1583,38 @@ impl Fragment { } } - let inline_start = if inline_start_is_some { + // Remove trailing whitespace from the inline-start split, if necessary. + // + // FIXME(pcwalton): Is there a more clever (i.e. faster) way to do this? + strip_trailing_whitespace(&**text_fragment_info.run, &mut inline_start_range); + + // Remove leading whitespace from the inline-end split, if necessary. + // + // FIXME(pcwalton): Is there a more clever (i.e. faster) way to do this? + if let Some(ref mut inline_end_range) = inline_end_range { + let inline_end_fragment_text = + text_fragment_info.run.text.slice_chars(inline_end_range.begin().to_uint(), + inline_end_range.end().to_uint()); + let mut leading_whitespace_character_count = 0i; + for ch in inline_end_fragment_text.chars() { + if ch.is_whitespace() { + leading_whitespace_character_count += 1 + } else { + break + } + } + inline_end_range.adjust_by(CharIndex(leading_whitespace_character_count), + -CharIndex(leading_whitespace_character_count)); + } + + // Normalize our split so that the inline-end fragment will never be `Some` while the + // inline-start fragment is `None`. + if inline_start_range.is_empty() && inline_end_range.is_some() { + inline_start_range = inline_end_range.unwrap(); + inline_end_range = None + } + + let inline_start = if !inline_start_range.is_empty() { Some(SplitInfo::new(inline_start_range, &**text_fragment_info)) } else { None @@ -1563,6 +1630,24 @@ impl Fragment { }) } + /// Attempts to strip trailing whitespace from this fragment by adjusting the text run range. + /// Returns true if any modifications were made. + pub fn strip_trailing_whitespace_if_necessary(&mut self) -> bool { + let text_fragment_info = + if let SpecificFragmentInfo::ScannedText(ref mut text_fragment_info) = self.specific { + text_fragment_info + } else { + return false + }; + + let run = text_fragment_info.run.clone(); + if strip_trailing_whitespace(&**run, &mut text_fragment_info.range) { + self.border_box.size.inline = run.advance_for_range(&text_fragment_info.range); + return true + } + false + } + /// Returns true if this fragment is an unscanned text fragment that consists entirely of /// whitespace that should be stripped. pub fn is_ignorable_whitespace(&self) -> bool { @@ -1982,3 +2067,25 @@ pub enum CoordinateSystem { Self, } +/// Given a range and a text run, adjusts the range to eliminate trailing whitespace. Returns true +/// if any modifications were made. +fn strip_trailing_whitespace(text_run: &TextRun, range: &mut Range<CharIndex>) -> bool { + // FIXME(pcwalton): Is there a more clever (i.e. faster) way to do this? + let text = text_run.text.slice_chars(range.begin().to_uint(), range.end().to_uint()); + let mut trailing_whitespace_character_count = 0i; + for ch in text.chars().rev() { + if ch.is_whitespace() { + trailing_whitespace_character_count += 1 + } else { + break + } + } + + if trailing_whitespace_character_count == 0 { + return false + } + + range.extend_by(-CharIndex(trailing_whitespace_character_count)); + return true +} + diff --git a/components/layout/inline.rs b/components/layout/inline.rs index ac10e813e77..c864b56e111 100644 --- a/components/layout/inline.rs +++ b/components/layout/inline.rs @@ -34,7 +34,8 @@ use std::mem; use std::num::ToPrimitive; use std::ops::{Add, Sub, Mul, Div, Rem, Neg, Shl, Shr, Not, BitOr, BitAnd, BitXor}; use std::u16; -use style::computed_values::{overflow, text_align, text_overflow, vertical_align, white_space}; +use style::computed_values::{overflow, text_align, text_justify, text_overflow, vertical_align}; +use style::computed_values::{white_space}; use style::ComputedValues; use std::sync::Arc; @@ -301,6 +302,18 @@ impl LineBreaker { self.lines.len()); self.flush_current_line() } + + // Strip trailing whitespace from the last line if necessary. + if let Some(ref mut last_line) = self.lines.last_mut() { + if let Some(ref mut last_fragment) = self.new_fragments.last_mut() { + let previous_inline_size = last_line.bounds.size.inline - + last_fragment.border_box.size.inline; + if last_fragment.strip_trailing_whitespace_if_necessary() { + last_line.bounds.size.inline = previous_inline_size + + last_fragment.border_box.size.inline; + } + } + } } /// Acquires a new fragment to lay out from the work list or fragment list as appropriate. @@ -534,7 +547,7 @@ impl LineBreaker { /// Tries to append the given fragment to the line, splitting it if necessary. Returns true if /// we successfully pushed the fragment to the line or false if we couldn't. fn append_fragment_to_line_if_possible(&mut self, - fragment: Fragment, + mut fragment: Fragment, flow: &InlineFlow, layout_context: &LayoutContext, flags: InlineReflowFlags) @@ -546,7 +559,8 @@ impl LineBreaker { self.pending_line.green_zone = line_bounds.size; } - debug!("LineBreaker: trying to append to line {} (fragment size: {:?}, green zone: {:?}): {:?}", + debug!("LineBreaker: trying to append to line {} (fragment size: {:?}, green zone: {:?}): \ + {:?}", self.lines.len(), fragment.border_box.size, self.pending_line.green_zone, @@ -581,7 +595,7 @@ impl LineBreaker { debug!("LineBreaker: fragment can't split and line {} is empty, so overflowing", self.lines.len()); self.push_fragment_to_line(layout_context, fragment); - return true + return false } // Split it up! @@ -609,13 +623,15 @@ impl LineBreaker { // Push the first fragment onto the line we're working on and start off the next line with // the second fragment. If there's no second fragment, the next line will start off empty. match (inline_start_fragment, inline_end_fragment) { - (Some(inline_start_fragment), Some(inline_end_fragment)) => { + (Some(inline_start_fragment), Some(mut inline_end_fragment)) => { self.push_fragment_to_line(layout_context, inline_start_fragment); + self.flush_current_line(); self.work_list.push_front(inline_end_fragment) }, - (Some(fragment), None) | (None, Some(fragment)) => { - self.push_fragment_to_line(layout_context, fragment) + (Some(mut fragment), None) => { + self.push_fragment_to_line(layout_context, fragment); } + (None, Some(_)) => debug_assert!(false, "un-normalized split result"), (None, None) => {} } @@ -856,25 +872,40 @@ impl InlineFlow { } } - /// Sets fragment positions in the inline direction based on alignment for one line. + /// Sets fragment positions in the inline direction based on alignment for one line. This + /// performs text justification if mandated by the style. fn set_inline_fragment_positions(fragments: &mut InlineFragments, line: &Line, line_align: text_align::T, - indentation: Au) { + indentation: Au, + is_last_line: bool) { // Figure out how much inline-size we have. let slack_inline_size = max(Au(0), line.green_zone.inline - line.bounds.size.inline); - // Set the fragment inline positions based on that alignment. - let mut inline_start_position_for_fragment = line.bounds.start.i + indentation + - match line_align { - // So sorry, but justified text is more complicated than shuffling line - // coordinates. - // - // TODO(burg, issue #213): Implement `text-align: justify`. - text_align::T::left | text_align::T::justify => Au(0), - text_align::T::center => slack_inline_size.scale_by(0.5), - text_align::T::right => slack_inline_size, - }; + // Compute the value we're going to use for `text-justify`. + let text_justify = if fragments.fragments.is_empty() { + return + } else { + fragments.fragments[0].style().get_inheritedtext().text_justify + }; + + // Set the fragment inline positions based on that alignment, and justify the text if + // necessary. + let mut inline_start_position_for_fragment = line.bounds.start.i + indentation; + match line_align { + text_align::T::justify if !is_last_line && text_justify != text_justify::T::none => { + InlineFlow::justify_inline_fragments(fragments, line, slack_inline_size) + } + text_align::T::left | text_align::T::justify => {} + text_align::T::center => { + inline_start_position_for_fragment = inline_start_position_for_fragment + + slack_inline_size.scale_by(0.5) + } + text_align::T::right => { + inline_start_position_for_fragment = inline_start_position_for_fragment + + slack_inline_size + } + } for fragment_index in range(line.range.begin(), line.range.end()) { let fragment = fragments.get_mut(fragment_index.to_uint()); @@ -889,6 +920,75 @@ impl InlineFlow { } } + /// Justifies the given set of inline fragments, distributing the `slack_inline_size` among all + /// of them according to the value of `text-justify`. + fn justify_inline_fragments(fragments: &mut InlineFragments, + line: &Line, + slack_inline_size: Au) { + // Fast path. + if slack_inline_size == Au(0) { + return + } + + // First, calculate the number of expansion opportunities (spaces, normally). + let mut expansion_opportunities = 0i32; + for fragment_index in line.range.each_index() { + let fragment = fragments.get(fragment_index.to_uint()); + let scanned_text_fragment_info = + if let SpecificFragmentInfo::ScannedText(ref info) = fragment.specific { + info + } else { + continue + }; + for slice in scanned_text_fragment_info.run.character_slices_in_range( + &scanned_text_fragment_info.range) { + expansion_opportunities += slice.glyphs.space_count_in_range(&slice.range) as i32 + } + } + + // Then distribute all the space across the expansion opportunities. + let space_per_expansion_opportunity = slack_inline_size.to_subpx() / + (expansion_opportunities as f64); + for fragment_index in line.range.each_index() { + let fragment = fragments.get_mut(fragment_index.to_uint()); + let mut scanned_text_fragment_info = + if let SpecificFragmentInfo::ScannedText(ref mut info) = fragment.specific { + info + } else { + continue + }; + let fragment_range = scanned_text_fragment_info.range; + + // FIXME(pcwalton): This is an awful lot of uniqueness making. I don't see any easy way + // to get rid of it without regressing the performance of the non-justified case, + // though. + let run = scanned_text_fragment_info.run.make_unique(); + { + let glyph_runs = run.glyphs.make_unique(); + for mut glyph_run in glyph_runs.iter_mut() { + let mut range = glyph_run.range.intersect(&fragment_range); + if range.is_empty() { + continue + } + range.shift_by(-glyph_run.range.begin()); + + let glyph_store = glyph_run.glyph_store.make_unique(); + glyph_store.distribute_extra_space_in_range(&range, + space_per_expansion_opportunity); + } + } + + // Recompute the fragment's border box size. + let new_inline_size = run.advance_for_range(&fragment_range); + let new_size = LogicalSize::new(fragment.style.writing_mode, + new_inline_size, + fragment.border_box.size.block); + fragment.border_box = LogicalRect::from_point_size(fragment.style.writing_mode, + fragment.border_box.start, + new_size); + } + } + /// Sets final fragment positions in the block direction for one line. Assumes that the /// fragment positions were initially set to the distance from the baseline first. fn set_block_fragment_positions(fragments: &mut InlineFragments, @@ -1083,12 +1183,16 @@ impl Flow for InlineFlow { // Now, go through each line and lay out the fragments inside. let mut line_distance_from_flow_block_start = Au(0); - for line in self.lines.iter_mut() { - // Lay out fragments in the inline direction. + let line_count = self.lines.len(); + for line_index in range(0, line_count) { + let line = &mut self.lines[line_index]; + + // Lay out fragments in the inline direction, and justify them if necessary. InlineFlow::set_inline_fragment_positions(&mut self.fragments, line, self.base.flags.text_align(), - indentation); + indentation, + line_index + 1 == line_count); // Set the block-start position of the current line. // `line_height_offset` is updated at the end of the previous loop. diff --git a/components/script/dom/webidls/CSSStyleDeclaration.webidl b/components/script/dom/webidls/CSSStyleDeclaration.webidl index 70998624dc4..0d716d4ab15 100644 --- a/components/script/dom/webidls/CSSStyleDeclaration.webidl +++ b/components/script/dom/webidls/CSSStyleDeclaration.webidl @@ -121,6 +121,7 @@ partial interface CSSStyleDeclaration { [TreatNullAs=EmptyString] attribute DOMString textAlign; [TreatNullAs=EmptyString] attribute DOMString textDecoration; [TreatNullAs=EmptyString] attribute DOMString textIndent; + [TreatNullAs=EmptyString] attribute DOMString textJustify; [TreatNullAs=EmptyString] attribute DOMString textOrientation; [TreatNullAs=EmptyString] attribute DOMString textRendering; [TreatNullAs=EmptyString] attribute DOMString textTransform; diff --git a/components/style/properties/mod.rs.mako b/components/style/properties/mod.rs.mako index b63a37f4a5e..fe58a488f9b 100644 --- a/components/style/properties/mod.rs.mako +++ b/components/style/properties/mod.rs.mako @@ -1218,6 +1218,9 @@ pub mod longhands { ${single_keyword("text-overflow", "clip ellipsis")} + // TODO(pcwalton): Support `text-justify: distribute`. + ${single_keyword("text-justify", "auto none inter-word")} + ${new_style_struct("Text", is_inherited=False)} <%self:longhand name="text-decoration"> diff --git a/tests/ref/basic.list b/tests/ref/basic.list index 281a854d2d1..96c01f4b87e 100644 --- a/tests/ref/basic.list +++ b/tests/ref/basic.list @@ -241,3 +241,7 @@ fragment=top != ../html/acid2.html acid2_ref.html == mix_blend_mode_a.html mix_blend_mode_ref.html != text_overflow_a.html text_overflow_ref.html == floated_list_item_a.html floated_list_item_ref.html +== text_align_justify_a.html text_align_justify_ref.html +== text_justify_none_a.html text_justify_none_ref.html +== text_overflow_basic_a.html text_overflow_basic_ref.html +== text_align_complex_a.html text_align_complex_ref.html diff --git a/tests/ref/outlines_wrap_ref.html b/tests/ref/outlines_wrap_ref.html index db5c08893b5..5050e156f61 100644 --- a/tests/ref/outlines_wrap_ref.html +++ b/tests/ref/outlines_wrap_ref.html @@ -13,7 +13,7 @@ span { } </style> <body> -<section><span>I like </span><span>truffles</span></section> +<section><span>I like</span> <span>truffles</span></section> </body> </html> diff --git a/tests/ref/text_align_complex_a.html b/tests/ref/text_align_complex_a.html new file mode 100644 index 00000000000..24b20a072c5 --- /dev/null +++ b/tests/ref/text_align_complex_a.html @@ -0,0 +1,39 @@ +<!DOCTYPE html>
+<html>
+<head>
+<link rel="stylesheet" type="text/css" href="css/ahem.css">
+<style>
+
+ * { margin: 0px !important; padding: 0px !important; }
+
+ div {
+ width: 100px;
+ font-size: 10px;
+ font-family: Ahem, monospace;
+ padding: 10px !important;
+ }
+
+</style>
+</head>
+<body>
+
+<section style="text-align: right; color: #f00;">
+ <div style="background: #fdd;"> xx xx xx xxxx</div>
+ <div style="background: #fdd;"> xx xx xx xxxxxxxxxxxxx</div>
+ <div style="background: #fdd;">xxxxxxxxxxxxx xx xx xx xxxx</div>
+</section>
+
+<section style="text-align: center; color: #0f0;">
+ <div style="background: #dfd;"> xx xx xx xxxx </div>
+ <div style="background: #dfd;"> xx xx xx xxxxxxxxxxxxx</div>
+ <div style="background: #dfd;">xxxxxxxxxxxxx xx xx xx xxxx </div>
+</section>
+
+<section style="text-align: justify; color: #00f;">
+ <div style="background: #ddf;">xx xx xx xxxx</div>
+ <div style="background: #ddf;">xx xx xx xxxxxxxxxxxxx</div>
+ <div style="background: #ddf;">xxxxxxxxxxxxx xx xx xx xxxx </div>
+</section>
+</body>
+</html>
+
diff --git a/tests/ref/text_align_complex_ref.html b/tests/ref/text_align_complex_ref.html new file mode 100644 index 00000000000..b716c0265bc --- /dev/null +++ b/tests/ref/text_align_complex_ref.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> +<head> +<link rel="stylesheet" type="text/css" href="css/ahem.css"> +<style> + + * { margin: 0px !important; padding: 0px !important; } + + div { + width: 100px; + font-size: 10px; + font-family: Ahem, monospace; + padding: 10px !important; + } + + section.reference { text-align: left !important; } + section.reference > div { white-space: pre; } + +</style> +</head> +<body> + +<section class="reference" style="text-align: right; color: #f00;"> + <div style="background: #fdd;"> xx xx xx<br /> xxxx</div> + <div style="background: #fdd;"> xx xx xx<br />xxxxxxxxxxxxx</div> + <div style="background: #fdd;">xxxxxxxxxxxxx<br /> xx xx xx<br /> xxxx</div> +</section> + +<section class="reference" style="text-align: center; color: #0f0;"> + <div style="background: #dfd;"> xx xx xx <br /> xxxx </div> + <div style="background: #dfd;"> xx xx xx <br />xxxxxxxxxxxxx</div> + <div style="background: #dfd;">xxxxxxxxxxxxx<br /> xx xx xx <br /> xxxx </div> +</section> + +<section class="reference" style="text-align: justify; color: #00f;"> + <div style="background: #ddf;">xx xx xx<br />xxxx</div> + <div style="background: #ddf;">xx xx xx<br />xxxxxxxxxxxxx</div> + <div style="background: #ddf;">xxxxxxxxxxxxx<br />xx xx xx<br />xxxx </div> +</section> +</body> +</html> + diff --git a/tests/ref/text_align_justify_a.html b/tests/ref/text_align_justify_a.html new file mode 100644 index 00000000000..da1afc2f595 --- /dev/null +++ b/tests/ref/text_align_justify_a.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<link rel="stylesheet" type="text/css" href="css/ahem.css"> +<style> +p { + text-align: justify; + width: 135px; + font-size: 25px; + color: blue; + position: absolute; + top: 0; + left: 0; + margin: 0; +} +</style> +</head> +<body> +<p>X X X X X</p> +</body> +</html> + diff --git a/tests/ref/text_align_justify_ref.html b/tests/ref/text_align_justify_ref.html new file mode 100644 index 00000000000..556245af1ce --- /dev/null +++ b/tests/ref/text_align_justify_ref.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that basic `text-align: justify` works. --> +<style> +section { + background: blue; + position: absolute; + width: 25px; + height: 25px; +} +#a, #d { + left: 0; +} +#a, #b, #c { + top: 0; +} +#d, #e { + top: 25px; +} +#b { + left: 55px; +} +#c { + left: 110px; +} +#e { + left: 50px; +} +</style> +</head> +<body> +<section id=a></section> +<section id=b></section> +<section id=c></section> +<section id=d></section> +<section id=e></section> +</body> +</html> + + diff --git a/tests/ref/text_justify_none_a.html b/tests/ref/text_justify_none_a.html new file mode 100644 index 00000000000..b46cc654212 --- /dev/null +++ b/tests/ref/text_justify_none_a.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that `text-justify: none` disables justification. --> +<style> +p { + text-align: justify; + text-justify: none; +} +</style> +</head> +<body> +<p>MANY YEARS AGO PRINCE DARKNESS "GANNON" STOLE ONE OF THE TRIFORCE WITH POWER. PRINCESS ZELDA HAD ONE OF THE TRIFORCE WITH WISDOM. SHE DIVIDED IT INTO 8 UNITS TO HIDE IT FROM "GANNON" BEFORE SHE WAS CAPTURED. GO FIND THE "8" UNITS "LINK" TO SAVE HER.</p> +</body> +</html> diff --git a/tests/ref/text_justify_none_ref.html b/tests/ref/text_justify_none_ref.html new file mode 100644 index 00000000000..6b129f0cc14 --- /dev/null +++ b/tests/ref/text_justify_none_ref.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<!-- Tests that `text-justify: none` disables justification. --> +<style> +p { + text-align: left; +} +</style> +</head> +<body> +<p>MANY YEARS AGO PRINCE DARKNESS "GANNON" STOLE ONE OF THE TRIFORCE WITH POWER. PRINCESS ZELDA HAD ONE OF THE TRIFORCE WITH WISDOM. SHE DIVIDED IT INTO 8 UNITS TO HIDE IT FROM "GANNON" BEFORE SHE WAS CAPTURED. GO FIND THE "8" UNITS "LINK" TO SAVE HER.</p> +</body> +</html> diff --git a/tests/ref/text_overflow_basic_a.html b/tests/ref/text_overflow_basic_a.html new file mode 100644 index 00000000000..e8da344f467 --- /dev/null +++ b/tests/ref/text_overflow_basic_a.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<style> +section { + border: solid black 1px; + width: 10em; +} +</style> +</head> +<body> +<section>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx<br>asdf asdf asdf</section> +</body> +</html> + diff --git a/tests/ref/text_overflow_basic_ref.html b/tests/ref/text_overflow_basic_ref.html new file mode 100644 index 00000000000..a5f10212310 --- /dev/null +++ b/tests/ref/text_overflow_basic_ref.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<style> +section { + border: solid black 1px; + width: 10em; +} +</style> +</head> +<body> +<section>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx asdf asdf asdf</section> +</body> +</html> + |