layout: Implement word-spacing per CSS 2.1 § 16.4.

This assumes that there are no ligatures that span across multiple
words. Since we have a per-word shape cache, this is a safe assumption
as of now. I have left comments to ensure that, if and when this is
revisted, we make sure to handle it properly.
This commit is contained in:
Patrick Walton 2014-12-12 17:49:37 -08:00
parent ea39b878ac
commit 106fdb1d32
7 changed files with 94 additions and 7 deletions

View file

@ -115,6 +115,8 @@ pub struct ShapingOptions {
/// Spacing to add between each letter. Corresponds to the CSS 2.1 `letter-spacing` property.
/// NB: You will probably want to set the `IGNORE_LIGATURES_SHAPING_FLAG` if this is non-null.
pub letter_spacing: Option<Au>,
/// Spacing to add between each word. Corresponds to the CSS 2.1 `word-spacing` property.
pub word_spacing: Au,
/// Various flags.
pub flags: ShapingFlags,
}

View file

@ -42,8 +42,8 @@ use harfbuzz::{hb_shape, hb_buffer_get_glyph_infos};
use libc::{c_uint, c_int, c_void, c_char};
use servo_util::geometry::Au;
use servo_util::range::Range;
use std::mem;
use std::char;
use std::mem;
use std::cmp;
use std::ptr;
@ -436,8 +436,12 @@ impl Shaper {
// for now, just pretend that every character is a cluster start.
// (i.e., pretend there are no combining character sequences).
// 1-to-1 mapping of character to glyph also treated as ligature start.
//
// NB: When we acquire the ability to handle ligatures that cross word boundaries,
// we'll need to do something special to handle `word-spacing` properly.
let shape = glyph_data.get_entry_for_glyph(glyph_span.begin(), &mut y_pos);
let advance = self.advance_for_shaped_glyph(shape.advance, options);
let character = text.char_at(char_byte_span.begin() as uint);
let advance = self.advance_for_shaped_glyph(shape.advance, character, options);
let data = GlyphData::new(shape.codepoint,
advance,
shape.offset,
@ -488,11 +492,22 @@ impl Shaper {
glyphs.finalize_changes();
}
fn advance_for_shaped_glyph(&self, advance: Au, options: &ShapingOptions) -> Au {
fn advance_for_shaped_glyph(&self, mut advance: Au, character: char, options: &ShapingOptions)
-> Au {
match options.letter_spacing {
None => advance,
Some(spacing) => advance + spacing,
None => {}
Some(letter_spacing) => advance = advance + letter_spacing,
};
// CSS 2.1 § 16.4 states that "word spacing affects each space (U+0020) and non-breaking
// space (U+00A0) left in the text after the white space processing rules have been
// applied. The effect of the property on other word-separator characters is undefined."
// We elect to only space the two required code points.
if character == ' ' || character == '\u00a0' {
advance = advance + options.word_spacing
}
advance
}
}

View file

@ -107,16 +107,19 @@ impl TextRunScanner {
let compression;
let text_transform;
let letter_spacing;
let word_spacing;
{
let in_fragment = self.clump.front().unwrap();
let font_style = in_fragment.style().get_font_arc();
let inherited_text_style = in_fragment.style().get_inheritedtext();
fontgroup = font_context.get_layout_font_group_for_style(font_style);
compression = match in_fragment.white_space() {
white_space::normal | white_space::nowrap => CompressWhitespaceNewline,
white_space::pre => CompressNone,
};
text_transform = in_fragment.style().get_inheritedtext().text_transform;
letter_spacing = in_fragment.style().get_inheritedtext().letter_spacing;
text_transform = inherited_text_style.text_transform;
letter_spacing = inherited_text_style.letter_spacing;
word_spacing = inherited_text_style.word_spacing.unwrap_or(Au(0));
}
// First, transform/compress text of all the nodes.
@ -160,6 +163,7 @@ impl TextRunScanner {
// `fi n a l l y`.
let options = ShapingOptions {
letter_spacing: letter_spacing,
word_spacing: word_spacing,
flags: match letter_spacing {
Some(Au(0)) | None => ShapingFlags::empty(),
Some(_) => IGNORE_LIGATURES_SHAPING_FLAG,

View file

@ -1111,6 +1111,29 @@ pub mod longhands {
}
</%self:single_component_value>
<%self:single_component_value name="word-spacing">
pub type SpecifiedValue = Option<specified::Length>;
pub mod computed_value {
use super::super::Au;
pub type T = Option<Au>;
}
#[inline]
pub fn get_initial_value() -> computed_value::T {
None
}
#[inline]
pub fn to_computed_value(value: SpecifiedValue, context: &computed::Context)
-> computed_value::T {
value.map(|length| computed::compute_Au(length, context))
}
pub fn from_component_value(input: &ComponentValue, _: &Url) -> Result<SpecifiedValue,()> {
match input {
&Ident(ref value) if value.eq_ignore_ascii_case("normal") => Ok(None),
_ => specified::Length::parse_non_negative(input).map(|length| Some(length)),
}
}
</%self:single_component_value>
${predefined_type("text-indent", "LengthOrPercentage", "computed::LP_Length(Au(0))")}
${new_style_struct("Text", is_inherited=False)}

View file

@ -198,3 +198,4 @@ fragment=top != ../html/acid2.html acid2_ref.html
!= border_black_ridge.html border_black_solid.html
!= border_black_ridge.html border_black_groove.html
== text_indent_a.html text_indent_ref.html
== word_spacing_a.html word_spacing_ref.html

View file

@ -0,0 +1,16 @@
<html>
<head>
<!-- Tests that `word-spacing` works. -->
<link rel="stylesheet" type="text/css" href="css/ahem.css">
<style>
* {
word-spacing: 100px;
color: chartreuse;
}
body {
margin: 0;
}
</style>
<body>X XX</body>
</head>

View file

@ -0,0 +1,26 @@
<html>
<head>
<!-- Tests that `word-spacing` works. -->
<style>
section, nav, main {
background-color: chartreuse;
width: 100px;
height: 100px;
position: absolute;
top: 0;
}
section {
left: 0;
}
nav {
left: 300px;
}
main {
left: 400px;
}
</style>
<body><section></section><nav></nav><main></main></body>
</head>