mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
layout: Implement text-align: justify
and text-justify
per
CSS-TEXT-3 § 7.3. `text-justify: distribute` is not supported. The behavior of `text-justify: none` does not seem to match what Firefox and Chrome do, but it seems to match the spec. Closes #213.
This commit is contained in:
parent
f58a129251
commit
5fdaba05a6
17 changed files with 528 additions and 63 deletions
|
@ -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>>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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!();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
39
tests/ref/text_align_complex_a.html
Normal file
39
tests/ref/text_align_complex_a.html
Normal file
|
@ -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>
|
||||
|
42
tests/ref/text_align_complex_ref.html
Normal file
42
tests/ref/text_align_complex_ref.html
Normal file
|
@ -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>
|
||||
|
22
tests/ref/text_align_justify_a.html
Normal file
22
tests/ref/text_align_justify_a.html
Normal file
|
@ -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>
|
||||
|
41
tests/ref/text_align_justify_ref.html
Normal file
41
tests/ref/text_align_justify_ref.html
Normal file
|
@ -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>
|
||||
|
||||
|
15
tests/ref/text_justify_none_a.html
Normal file
15
tests/ref/text_justify_none_a.html
Normal file
|
@ -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>
|
14
tests/ref/text_justify_none_ref.html
Normal file
14
tests/ref/text_justify_none_ref.html
Normal file
|
@ -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>
|
15
tests/ref/text_overflow_basic_a.html
Normal file
15
tests/ref/text_overflow_basic_a.html
Normal file
|
@ -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>
|
||||
|
15
tests/ref/text_overflow_basic_ref.html
Normal file
15
tests/ref/text_overflow_basic_ref.html
Normal file
|
@ -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>
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue