diff --git a/servo/components/gfx/font.rs b/servo/components/gfx/font.rs index d7c28fb1d8c9..c2d4504e7204 100644 --- a/servo/components/gfx/font.rs +++ b/servo/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, - pub shape_cache: HashCache>, - pub glyph_advance_cache: HashCache, + pub shape_cache: HashCache>, + pub glyph_advance_cache: HashCache, +} + +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, + /// 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 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 { - self.make_shaper(); + pub fn shape_text(&mut self, text: &str, options: &ShapingOptions) -> Arc { + 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/servo/components/gfx/lib.rs b/servo/components/gfx/lib.rs index 943de358dddb..c680233f7acd 100644 --- a/servo/components/gfx/lib.rs +++ b/servo/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/servo/components/gfx/text/shaping/harfbuzz.rs b/servo/components/gfx/text/shaping/harfbuzz.rs index 23c39a195cad..aec15f0014f2 100644 --- a/servo/components/gfx/text/shaping/harfbuzz.rs +++ b/servo/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, } #[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/servo/components/gfx/text/shaping/mod.rs b/servo/components/gfx/text/shaping/mod.rs index 7fce60a3106f..79e5452db061 100644 --- a/servo/components/gfx/text/shaping/mod.rs +++ b/servo/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/servo/components/gfx/text/text_run.rs b/servo/components/gfx/text/text_run.rs index 4bb280ef261d..08d09bb48b56 100644 --- a/servo/components/gfx/text/text_run.rs +++ b/servo/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> 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 { + pub fn break_and_shape(font: &mut Font, text: &str, options: &ShapingOptions) + -> Vec { // 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/servo/components/layout/text.rs b/servo/components/layout/text.rs index c12119f4510e..ac96967522b7 100644 --- a/servo/components/layout/text.rs +++ b/servo/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/servo/components/style/properties/mod.rs.mako b/servo/components/style/properties/mod.rs.mako index e97db4c82249..2b9d9ae49719 100644 --- a/servo/components/style/properties/mod.rs.mako +++ b/servo/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; + pub mod computed_value { + use super::super::Au; + pub type T = Option; + } + #[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 { + match input { + &Ident(ref value) if value.eq_ignore_ascii_case("normal") => Ok(None), + _ => specified::Length::parse_non_negative(input).map(|length| Some(length)), + } + } + + ${new_style_struct("Text", is_inherited=False)} <%self:longhand name="text-decoration"> diff --git a/servo/components/util/geometry.rs b/servo/components/util/geometry.rs index 06b74c7c3a0b..1dfa1fe0f9db 100644 --- a/servo/components/util/geometry.rs +++ b/servo/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 {