diff options
-rw-r--r-- | components/gfx/font.rs | 84 | ||||
-rw-r--r-- | components/gfx/lib.rs | 2 | ||||
-rw-r--r-- | components/gfx/text/shaping/harfbuzz.rs | 84 | ||||
-rw-r--r-- | components/gfx/text/shaping/mod.rs | 3 | ||||
-rw-r--r-- | components/gfx/text/text_run.rs | 29 | ||||
-rw-r--r-- | components/layout/text.rs | 22 | ||||
-rw-r--r-- | components/style/properties/mod.rs.mako | 23 | ||||
-rw-r--r-- | components/util/geometry.rs | 2 | ||||
-rw-r--r-- | tests/ref/basic.list | 1 | ||||
-rw-r--r-- | tests/ref/letter_spacing_a.html | 17 | ||||
-rw-r--r-- | tests/ref/letter_spacing_ref.html | 26 |
11 files changed, 246 insertions, 47 deletions
diff --git a/components/gfx/font.rs b/components/gfx/font.rs index d7c28fb1d8c..c2d4504e720 100644 --- a/components/gfx/font.rs +++ b/components/gfx/font.rs @@ -13,9 +13,10 @@ use style::computed_values::{font_variant, font_weight}; use style::style_structs::Font as FontStyle; use sync::Arc; -use servo_util::geometry::Au; +use collections::hash::Hash; use platform::font_context::FontContextHandle; use platform::font::{FontHandle, FontTable}; +use servo_util::geometry::Au; use text::glyph::{GlyphStore, GlyphId}; use text::shaping::ShaperMethods; use text::{Shaper, TextRun}; @@ -95,37 +96,85 @@ pub struct Font { pub requested_pt_size: Au, pub actual_pt_size: Au, pub shaper: Option<Shaper>, - pub shape_cache: HashCache<String, Arc<GlyphStore>>, - pub glyph_advance_cache: HashCache<u32, FractionalPixel>, + pub shape_cache: HashCache<ShapeCacheEntry,Arc<GlyphStore>>, + pub glyph_advance_cache: HashCache<u32,FractionalPixel>, +} + +bitflags! { + flags ShapingFlags: u8 { + #[doc="Set if the text is entirely whitespace."] + const IS_WHITESPACE_SHAPING_FLAG = 0x01, + #[doc="Set if we are to ignore ligatures."] + const IGNORE_LIGATURES_SHAPING_FLAG = 0x02 + } +} + +/// Various options that control text shaping. +#[deriving(Clone, Eq, PartialEq, Hash)] +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>, + /// Various flags. + pub flags: ShapingFlags, +} + +/// An entry in the shape cache. +#[deriving(Clone, Eq, PartialEq, Hash)] +pub struct ShapeCacheEntry { + text: String, + options: ShapingOptions, +} + +#[deriving(Clone, Eq, PartialEq, Hash)] +struct ShapeCacheEntryRef<'a> { + text: &'a str, + options: &'a ShapingOptions, +} + +impl<'a> Equiv<ShapeCacheEntry> for ShapeCacheEntryRef<'a> { + fn equiv(&self, other: &ShapeCacheEntry) -> bool { + self.text == other.text.as_slice() && *self.options == other.options + } } impl Font { - pub fn shape_text(&mut self, text: &str, is_whitespace: bool) -> Arc<GlyphStore> { - self.make_shaper(); + pub fn shape_text(&mut self, text: &str, options: &ShapingOptions) -> Arc<GlyphStore> { + self.make_shaper(options); + let shaper = &self.shaper; - match self.shape_cache.find_equiv(text) { + let lookup_key = ShapeCacheEntryRef { + text: text, + options: options, + }; + match self.shape_cache.find_equiv(&lookup_key) { None => {} Some(glyphs) => return (*glyphs).clone(), } - let mut glyphs = GlyphStore::new(text.char_len() as int, is_whitespace); - shaper.as_ref().unwrap().shape_text(text, &mut glyphs); + let mut glyphs = GlyphStore::new(text.char_len() as int, + options.flags.contains(IS_WHITESPACE_SHAPING_FLAG)); + shaper.as_ref().unwrap().shape_text(text, options, &mut glyphs); + let glyphs = Arc::new(glyphs); - self.shape_cache.insert(text.to_string(), glyphs.clone()); + self.shape_cache.insert(ShapeCacheEntry { + text: text.to_string(), + options: *options, + }, glyphs.clone()); glyphs } - fn make_shaper<'a>(&'a mut self) -> &'a Shaper { + fn make_shaper<'a>(&'a mut self, options: &ShapingOptions) -> &'a Shaper { // fast path: already created a shaper match self.shaper { - Some(ref shaper) => { - let s: &'a Shaper = shaper; - return s; + Some(ref mut shaper) => { + shaper.set_options(options); + return shaper }, None => {} } - let shaper = Shaper::new(self); + let shaper = Shaper::new(self, options); self.shaper = Some(shaper); self.shaper.as_ref().unwrap() } @@ -149,7 +198,8 @@ impl Font { self.handle.glyph_index(codepoint) } - pub fn glyph_h_kerning(&mut self, first_glyph: GlyphId, second_glyph: GlyphId) -> FractionalPixel { + pub fn glyph_h_kerning(&mut self, first_glyph: GlyphId, second_glyph: GlyphId) + -> FractionalPixel { self.handle.glyph_h_kerning(first_glyph, second_glyph) } @@ -175,11 +225,11 @@ impl FontGroup { } } - pub fn create_textrun(&self, text: String) -> TextRun { + 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. - TextRun::new(&mut *self.fonts.get(0).borrow_mut(), text.clone()) + TextRun::new(&mut *self.fonts.get(0).borrow_mut(), text.clone(), options) } } diff --git a/components/gfx/lib.rs b/components/gfx/lib.rs index 943de358ddd..c680233f7ac 100644 --- a/components/gfx/lib.rs +++ b/components/gfx/lib.rs @@ -2,7 +2,7 @@ * 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/. */ -#![feature(globs, macro_rules, phase, unsafe_destructor)] +#![feature(globs, macro_rules, phase, unsafe_destructor, default_type_params)] #![deny(unused_imports)] #![deny(unused_variables)] diff --git a/components/gfx/text/shaping/harfbuzz.rs b/components/gfx/text/shaping/harfbuzz.rs index 23c39a195ca..aec15f0014f 100644 --- a/components/gfx/text/shaping/harfbuzz.rs +++ b/components/gfx/text/shaping/harfbuzz.rs @@ -4,7 +4,8 @@ extern crate harfbuzz; -use font::{Font, FontHandleMethods, FontTableMethods, FontTableTag}; +use font::{Font, FontHandleMethods, FontTableMethods, FontTableTag, IGNORE_LIGATURES_SHAPING_FLAG}; +use font::{ShapingOptions}; use platform::font::FontTable; use text::glyph::{CharIndex, GlyphStore, GlyphId, GlyphData}; use text::shaping::ShaperMethods; @@ -18,9 +19,11 @@ use harfbuzz::{hb_bool_t}; use harfbuzz::{hb_buffer_add_utf8}; use harfbuzz::{hb_buffer_destroy}; use harfbuzz::{hb_buffer_get_glyph_positions}; +use harfbuzz::{hb_buffer_get_length}; use harfbuzz::{hb_buffer_set_direction}; use harfbuzz::{hb_face_destroy}; use harfbuzz::{hb_face_t, hb_font_t}; +use harfbuzz::{hb_feature_t}; use harfbuzz::{hb_font_create}; use harfbuzz::{hb_font_destroy, hb_buffer_create}; use harfbuzz::{hb_font_funcs_create}; @@ -47,6 +50,9 @@ use std::ptr; static NO_GLYPH: i32 = -1; static CONTINUATION_BYTE: i32 = -2; +static LIGA: u32 = ((b'l' as u32) << 24) | ((b'i' as u32) << 16) | ((b'g' as u32) << 8) | + (b'a' as u32); + pub struct ShapedGlyphData { count: int, glyph_infos: *mut hb_glyph_info_t, @@ -131,10 +137,16 @@ impl ShapedGlyphData { } } +struct FontAndShapingOptions { + font: *mut Font, + options: ShapingOptions, +} + pub struct Shaper { hb_face: *mut hb_face_t, hb_font: *mut hb_font_t, hb_funcs: *mut hb_font_funcs_t, + font_and_shaping_options: Box<FontAndShapingOptions>, } #[unsafe_destructor] @@ -154,13 +166,18 @@ impl Drop for Shaper { } impl Shaper { - pub fn new(font: &mut Font) -> Shaper { + pub fn new(font: &mut Font, options: &ShapingOptions) -> Shaper { unsafe { - // Indirection for Rust Issue #6248, dynamic freeze scope artificially extended - let font_ptr = font as *mut Font; - let hb_face: *mut hb_face_t = hb_face_create_for_tables(get_font_table_func, - font_ptr as *mut c_void, - None); + let mut font_and_shaping_options = box FontAndShapingOptions { + font: font, + options: *options, + }; + let hb_face: *mut hb_face_t = + hb_face_create_for_tables(get_font_table_func, + (&mut *font_and_shaping_options) + as *mut FontAndShapingOptions + as *mut c_void, + None); let hb_font: *mut hb_font_t = hb_font_create(hb_face); // Set points-per-em. if zero, performs no hinting in that direction. @@ -178,16 +195,21 @@ impl Shaper { hb_font_funcs_set_glyph_func(hb_funcs, glyph_func, ptr::null_mut(), None); hb_font_funcs_set_glyph_h_advance_func(hb_funcs, glyph_h_advance_func, ptr::null_mut(), None); hb_font_funcs_set_glyph_h_kerning_func(hb_funcs, glyph_h_kerning_func, ptr::null_mut(), ptr::null_mut()); - hb_font_set_funcs(hb_font, hb_funcs, font_ptr as *mut c_void, None); + hb_font_set_funcs(hb_font, hb_funcs, font as *mut Font as *mut c_void, None); Shaper { hb_face: hb_face, hb_font: hb_font, hb_funcs: hb_funcs, + font_and_shaping_options: font_and_shaping_options, } } } + pub fn set_options(&mut self, options: &ShapingOptions) { + self.font_and_shaping_options.options = *options + } + fn float_to_fixed(f: f64) -> i32 { float_to_fixed(16, f) } @@ -200,7 +222,7 @@ impl Shaper { impl ShaperMethods for Shaper { /// Calculate the layout metrics associated with the given text when painted in a specific /// font. - fn shape_text(&self, text: &str, glyphs: &mut GlyphStore) { + fn shape_text(&self, text: &str, options: &ShapingOptions, glyphs: &mut GlyphStore) { unsafe { let hb_buffer: *mut hb_buffer_t = hb_buffer_create(); hb_buffer_set_direction(hb_buffer, HB_DIRECTION_LTR); @@ -211,15 +233,29 @@ impl ShaperMethods for Shaper { 0, text.len() as c_int); - hb_shape(self.hb_font, hb_buffer, ptr::null_mut(), 0); - self.save_glyph_results(text, glyphs, hb_buffer); + let mut features = Vec::new(); + if options.flags.contains(IGNORE_LIGATURES_SHAPING_FLAG) { + features.push(hb_feature_t { + _tag: LIGA, + _value: 0, + _start: 0, + _end: hb_buffer_get_length(hb_buffer), + }) + } + + hb_shape(self.hb_font, hb_buffer, features.as_mut_ptr(), features.len() as u32); + self.save_glyph_results(text, options, glyphs, hb_buffer); hb_buffer_destroy(hb_buffer); } } } impl Shaper { - fn save_glyph_results(&self, text: &str, glyphs: &mut GlyphStore, buffer: *mut hb_buffer_t) { + fn save_glyph_results(&self, + text: &str, + options: &ShapingOptions, + glyphs: &mut GlyphStore, + buffer: *mut hb_buffer_t) { let glyph_data = ShapedGlyphData::new(buffer); let glyph_count = glyph_data.len(); let byte_max = text.len() as int; @@ -401,8 +437,9 @@ impl Shaper { // (i.e., pretend there are no combining character sequences). // 1-to-1 mapping of character to glyph also treated as ligature start. 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 data = GlyphData::new(shape.codepoint, - shape.advance, + advance, shape.offset, false, true, @@ -450,6 +487,13 @@ impl Shaper { // lookup table for finding detailed glyphs by associated char index. glyphs.finalize_changes(); } + + fn advance_for_shaped_glyph(&self, advance: Au, options: &ShapingOptions) -> Au { + match options.letter_spacing { + None => advance, + Some(spacing) => advance + spacing, + } + } } /// Callbacks from Harfbuzz when font map and glyph advance lookup needed. @@ -504,13 +548,19 @@ extern fn glyph_h_kerning_func(_: *mut hb_font_t, } // Callback to get a font table out of a font. -extern fn get_font_table_func(_: *mut hb_face_t, tag: hb_tag_t, user_data: *mut c_void) -> *mut hb_blob_t { +extern fn get_font_table_func(_: *mut hb_face_t, + tag: hb_tag_t, + user_data: *mut c_void) + -> *mut hb_blob_t { unsafe { - let font: *const Font = user_data as *const Font; - assert!(font.is_not_null()); + // NB: These asserts have security implications. + let font_and_shaping_options: *const FontAndShapingOptions = + user_data as *const FontAndShapingOptions; + assert!(font_and_shaping_options.is_not_null()); + assert!((*font_and_shaping_options).font.is_not_null()); // TODO(Issue #197): reuse font table data, which will change the unsound trickery here. - match (*font).get_table_for_tag(tag as FontTableTag) { + match (*(*font_and_shaping_options).font).get_table_for_tag(tag as FontTableTag) { None => ptr::null_mut(), Some(ref font_table) => { let skinny_font_table_ptr: *const FontTable = font_table; // private context diff --git a/components/gfx/text/shaping/mod.rs b/components/gfx/text/shaping/mod.rs index 7fce60a3106..79e5452db06 100644 --- a/components/gfx/text/shaping/mod.rs +++ b/components/gfx/text/shaping/mod.rs @@ -7,6 +7,7 @@ //! //! Currently, only harfbuzz bindings are implemented. +use font::ShapingOptions; use text::glyph::GlyphStore; pub use text::shaping::harfbuzz::Shaper; @@ -14,6 +15,6 @@ pub use text::shaping::harfbuzz::Shaper; pub mod harfbuzz; pub trait ShaperMethods { - fn shape_text(&self, text: &str, glyphs: &mut GlyphStore); + fn shape_text(&self, text: &str, options: &ShapingOptions, glyphs: &mut GlyphStore); } diff --git a/components/gfx/text/text_run.rs b/components/gfx/text/text_run.rs index 4bb280ef261..08d09bb48b5 100644 --- a/components/gfx/text/text_run.rs +++ b/components/gfx/text/text_run.rs @@ -2,15 +2,15 @@ * 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 font::{Font, RunMetrics, FontMetrics}; +use font::{Font, FontHandleMethods, FontMetrics, IS_WHITESPACE_SHAPING_FLAG, RunMetrics}; +use font::{ShapingOptions}; +use platform::font_template::FontTemplateData; use servo_util::geometry::Au; use servo_util::range::Range; use servo_util::vec::{Comparator, FullBinarySearchMethods}; use std::slice::Items; use sync::Arc; use text::glyph::{CharIndex, GlyphStore}; -use font::FontHandleMethods; -use platform::font_template::FontTemplateData; /// A single "paragraph" of text in one font size and style. #[deriving(Clone)] @@ -117,8 +117,8 @@ impl<'a> Iterator<Range<CharIndex>> for LineIterator<'a> { } impl<'a> TextRun { - pub fn new(font: &mut Font, text: String) -> TextRun { - let glyphs = TextRun::break_and_shape(font, text.as_slice()); + pub fn new(font: &mut Font, text: String, options: &ShapingOptions) -> TextRun { + let glyphs = TextRun::break_and_shape(font, text.as_slice(), options); let run = TextRun { text: Arc::new(text), font_metrics: font.metrics.clone(), @@ -129,7 +129,8 @@ impl<'a> TextRun { return run; } - pub fn break_and_shape(font: &mut Font, text: &str) -> Vec<GlyphRun> { + pub fn break_and_shape(font: &mut Font, text: &str, options: &ShapingOptions) + -> Vec<GlyphRun> { // TODO(Issue #230): do a better job. See Gecko's LineBreaker. let mut glyphs = vec!(); let (mut byte_i, mut char_i) = (0u, CharIndex(0)); @@ -165,8 +166,14 @@ impl<'a> TextRun { let slice = text.slice(byte_last_boundary, byte_i); debug!("creating glyph store for slice {} (ws? {}), {} - {} in run {}", slice, !cur_slice_is_whitespace, byte_last_boundary, byte_i, text); + + let mut options = *options; + if !cur_slice_is_whitespace { + options.flags.insert(IS_WHITESPACE_SHAPING_FLAG); + } + glyphs.push(GlyphRun { - glyph_store: font.shape_text(slice, !cur_slice_is_whitespace), + glyph_store: font.shape_text(slice, &options), range: Range::new(char_last_boundary, char_i - char_last_boundary), }); byte_last_boundary = byte_i; @@ -182,8 +189,14 @@ impl<'a> TextRun { let slice = text.slice_from(byte_last_boundary); debug!("creating glyph store for final slice {} (ws? {}), {} - {} in run {}", slice, cur_slice_is_whitespace, byte_last_boundary, text.len(), text); + + let mut options = *options; + if cur_slice_is_whitespace { + options.flags.insert(IS_WHITESPACE_SHAPING_FLAG); + } + glyphs.push(GlyphRun { - glyph_store: font.shape_text(slice, cur_slice_is_whitespace), + glyph_store: font.shape_text(slice, &options), range: Range::new(char_last_boundary, char_i - char_last_boundary), }); } diff --git a/components/layout/text.rs b/components/layout/text.rs index c12119f4510..ac96967522b 100644 --- a/components/layout/text.rs +++ b/components/layout/text.rs @@ -9,7 +9,8 @@ use fragment::{Fragment, ScannedTextFragmentInfo, UnscannedTextFragment}; use inline::InlineFragments; -use gfx::font::{FontMetrics,RunMetrics}; +use gfx::font::{FontMetrics, IGNORE_LIGATURES_SHAPING_FLAG, RunMetrics, ShapingFlags}; +use gfx::font::{ShapingOptions}; use gfx::font_context::FontContext; use gfx::text::glyph::CharIndex; use gfx::text::text_run::TextRun; @@ -105,6 +106,7 @@ impl TextRunScanner { let fontgroup; let compression; let text_transform; + let letter_spacing; { let in_fragment = self.clump.front().unwrap(); let font_style = in_fragment.style().get_font_arc(); @@ -114,6 +116,7 @@ impl TextRunScanner { white_space::pre => CompressNone, }; text_transform = in_fragment.style().get_inheritedtext().text_transform; + letter_spacing = in_fragment.style().get_inheritedtext().letter_spacing; } // First, transform/compress text of all the nodes. @@ -150,7 +153,22 @@ impl TextRunScanner { self.clump = DList::new(); return last_whitespace } - Arc::new(box TextRun::new(&mut *fontgroup.fonts.get(0).borrow_mut(), run_text)) + + // 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 + // `fi n a l l y`. + let options = ShapingOptions { + letter_spacing: letter_spacing, + flags: match letter_spacing { + Some(Au(0)) | None => ShapingFlags::empty(), + Some(_) => IGNORE_LIGATURES_SHAPING_FLAG, + }, + }; + + Arc::new(box TextRun::new(&mut *fontgroup.fonts.get(0).borrow_mut(), + run_text, + &options)) }; // Make new fragments with the run and adjusted text indices. diff --git a/components/style/properties/mod.rs.mako b/components/style/properties/mod.rs.mako index e97db4c8224..2b9d9ae4971 100644 --- a/components/style/properties/mod.rs.mako +++ b/components/style/properties/mod.rs.mako @@ -1088,6 +1088,29 @@ pub mod longhands { // TODO: initial value should be 'start' (CSS Text Level 3, direction-dependent.) ${single_keyword("text-align", "left right center justify")} + <%self:single_component_value name="letter-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> + ${new_style_struct("Text", is_inherited=False)} <%self:longhand name="text-decoration"> diff --git a/components/util/geometry.rs b/components/util/geometry.rs index 06b74c7c3a0..1dfa1fe0f9d 100644 --- a/components/util/geometry.rs +++ b/components/util/geometry.rs @@ -64,7 +64,7 @@ pub enum PagePx {} // See https://bugzilla.mozilla.org/show_bug.cgi?id=177805 for more info. // // FIXME: Implement Au using Length and ScaleFactor instead of a custom type. -#[deriving(Clone, PartialEq, PartialOrd, Eq, Ord, Zero)] +#[deriving(Clone, Hash, PartialEq, PartialOrd, Eq, Ord, Zero)] pub struct Au(pub i32); impl Default for Au { diff --git a/tests/ref/basic.list b/tests/ref/basic.list index f06e562d94b..f1de5d65c41 100644 --- a/tests/ref/basic.list +++ b/tests/ref/basic.list @@ -193,3 +193,4 @@ fragment=top != ../html/acid2.html acid2_ref.html == text_transform_capitalize_a.html text_transform_capitalize_ref.html == outlines_simple_a.html outlines_simple_ref.html == outlines_wrap_a.html outlines_wrap_ref.html +== letter_spacing_a.html letter_spacing_ref.html diff --git a/tests/ref/letter_spacing_a.html b/tests/ref/letter_spacing_a.html new file mode 100644 index 00000000000..f0169810c47 --- /dev/null +++ b/tests/ref/letter_spacing_a.html @@ -0,0 +1,17 @@ +<html> +<head> +<!-- Tests that `letter-spacing` works. --> +<link rel="stylesheet" type="text/css" href="css/ahem.css"> +<style> +* { + letter-spacing: 100px; + color: blue; +} +body { + margin: 0; +} +</style> +<body>XXX</body> +</head> + + diff --git a/tests/ref/letter_spacing_ref.html b/tests/ref/letter_spacing_ref.html new file mode 100644 index 00000000000..46c65aa8394 --- /dev/null +++ b/tests/ref/letter_spacing_ref.html @@ -0,0 +1,26 @@ +<html> +<head> +<!-- Tests that `letter-spacing` works. --> +<style> +section, nav, main { + background-color: blue; + width: 100px; + height: 100px; + position: absolute; + top: 0; +} +section { + left: 0; +} +nav { + left: 200px; +} +main { + left: 400px; +} +</style> +<body><section></section><nav></nav><main></main></body> +</head> + + + |