From d01bc5d002eafb3ccc7cc6209ac6f1f27fe1d4de Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Fri, 17 Jul 2020 12:39:42 -0700 Subject: [PATCH] Make the fields of `TextMetrics` lazily calculated, and add an API that eliminates double layouts. This adds an extension to the HTML canvas API that allows you to pass the `TextMetrics` object returned by `measure_text()` to `fill_text()` and/or `stroke_text()` to draw the measured text without laying it out again. It improves performance on the `canvas_nanovg` demo. --- c/src/lib.rs | 2 +- canvas/src/text.rs | 555 +++++++++++++++++------------ examples/canvas_nanovg/src/main.rs | 44 +-- text/src/lib.rs | 114 ++++-- 4 files changed, 431 insertions(+), 284 deletions(-) diff --git a/c/src/lib.rs b/c/src/lib.rs index d216e759..5aebb4dc 100644 --- a/c/src/lib.rs +++ b/c/src/lib.rs @@ -826,7 +826,7 @@ trait TextMetricsExt { impl TextMetricsExt for TextMetrics { fn to_c(&self) -> PFTextMetrics { - PFTextMetrics { width: self.width } + PFTextMetrics { width: self.width() } } } diff --git a/canvas/src/text.rs b/canvas/src/text.rs index 2192413e..2ce638e0 100644 --- a/canvas/src/text.rs +++ b/canvas/src/text.rs @@ -22,66 +22,59 @@ use pathfinder_geometry::util; use pathfinder_geometry::vector::{Vector2F, vec2f}; use pathfinder_renderer::paint::PaintId; use pathfinder_text::{FontContext, FontRenderOptions, TextRenderMode}; -use skribo::{FontCollection, FontFamily, FontRef, Layout, TextStyle}; -use std::cell::RefCell; +use skribo::{FontCollection, FontFamily, FontRef, Layout as SkriboLayout, TextStyle}; +use std::borrow::Cow; +use std::cell::{Cell, RefCell}; use std::rc::Rc; use std::sync::Arc; impl CanvasRenderingContext2D { - pub fn fill_text(&mut self, string: &str, position: Vector2F) { + /// Fills the given text using the current style. + /// + /// As an extension, you may pass in the `TextMetrics` object returned by `measure_text()` to + /// fill the text that you passed into `measure_text()` with the layout-related style + /// properties set at the time you called that function. This allows Pathfinder to skip having + /// to lay out the text again. + pub fn fill_text(&mut self, text: &T, position: Vector2F) where T: ToTextLayout + ?Sized { let paint = self.current_state.resolve_paint(&self.current_state.fill_paint); let paint_id = self.canvas.scene.push_paint(&paint); - self.fill_or_stroke_text(string, position, paint_id, TextRenderMode::Fill); + self.fill_or_stroke_text(text, position, paint_id, TextRenderMode::Fill); } - pub fn stroke_text(&mut self, string: &str, position: Vector2F) { + /// Strokes the given text using the current style. + /// + /// As an extension, you may pass in the `TextMetrics` object returned by `measure_text()` to + /// stroke the text that you passed into `measure_text()` with the layout-related style + /// properties set at the time you called that function. This allows Pathfinder to skip having + /// to lay out the text again. + pub fn stroke_text(&mut self, text: &T, position: Vector2F) where T: ToTextLayout + ?Sized { let paint = self.current_state.resolve_paint(&self.current_state.stroke_paint); let paint_id = self.canvas.scene.push_paint(&paint); let render_mode = TextRenderMode::Stroke(self.current_state.resolve_stroke_style()); - self.fill_or_stroke_text(string, position, paint_id, render_mode); + self.fill_or_stroke_text(text, position, paint_id, render_mode); } - pub fn measure_text(&self, string: &str) -> TextMetrics { - let mut metrics = self.layout_text(string).metrics(); - metrics.make_origin_relative(&self.current_state); - metrics + /// Returns metrics of the given text using the current style. + /// + /// As an extension, the returned `TextMetrics` object contains all the layout data for the + /// string and can be used in its place when calling `fill_text()` and `stroke_text()` to avoid + /// needlessly performing layout multiple times. + pub fn measure_text(&self, text: &T) -> TextMetrics where T: ToTextLayout + ?Sized { + text.layout(CanvasState(&self.current_state)).into_owned() } - pub fn fill_layout(&mut self, layout: &Layout, transform: Transform2F) { - let paint_id = self.canvas.scene.push_paint(&self.current_state.fill_paint); + fn fill_or_stroke_text(&mut self, + text: &T, + mut position: Vector2F, + paint_id: PaintId, + render_mode: TextRenderMode) + where T: ToTextLayout + ?Sized { + let layout = text.layout(CanvasState(&self.current_state)); let clip_path = self.current_state.clip_path; let blend_mode = self.current_state.global_composite_operation.to_blend_mode(); - // TODO(pcwalton): Report errors. - drop(self.canvas_font_context - .0 - .borrow_mut() - .font_context - .push_layout(&mut self.canvas.scene, - &layout, - &TextStyle { size: self.current_state.font_size }, - &FontRenderOptions { - transform: transform * self.current_state.transform, - render_mode: TextRenderMode::Fill, - hinting_options: HintingOptions::None, - clip_path, - blend_mode, - paint_id, - })); - } - - fn fill_or_stroke_text(&mut self, - string: &str, - mut position: Vector2F, - paint_id: PaintId, - render_mode: TextRenderMode) { - let layout = self.layout_text(string); - - let clip_path = self.current_state.clip_path; - let blend_mode = self.current_state.global_composite_operation.to_blend_mode(); - - position += layout.metrics().text_origin(&self.current_state); + position += layout.text_origin(); let transform = self.current_state.transform * Transform2F::from_translation(position); // TODO(pcwalton): Report errors. @@ -90,8 +83,8 @@ impl CanvasRenderingContext2D { .borrow_mut() .font_context .push_layout(&mut self.canvas.scene, - &layout, - &TextStyle { size: self.current_state.font_size }, + &layout.skribo_layout, + &TextStyle { size: layout.font_size }, &FontRenderOptions { transform, render_mode, @@ -102,12 +95,6 @@ impl CanvasRenderingContext2D { })); } - fn layout_text(&self, string: &str) -> Layout { - skribo::layout(&TextStyle { size: self.current_state.font_size }, - &self.current_state.font_collection, - string) - } - // Text styles #[inline] @@ -152,46 +139,49 @@ impl CanvasRenderingContext2D { } } -/// Represents the dimensions of a piece of text in the canvas. -#[derive(Clone, Copy, Debug)] -pub struct TextMetrics { - /// The calculated width of a segment of inline text in pixels. - pub width: f32, - /// The distance from the alignment point given by the `text_align` state to the left side of - /// the bounding rectangle of the given text, in pixels. The distance is measured parallel to - /// the baseline. - pub actual_bounding_box_left: f32, - /// The distance from the alignment point given by the `text_align` state to the right side of - /// the bounding rectangle of the given text, in pixels. The distance is measured parallel to - /// the baseline. - pub actual_bounding_box_right: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the top of - /// the highest bounding rectangle of all the fonts used to render the text, in pixels. - pub font_bounding_box_ascent: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the bottom - /// of the highest bounding rectangle of all the fonts used to render the text, in pixels. - pub font_bounding_box_descent: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the top of - /// the bounding rectangle used to render the text, in pixels. - pub actual_bounding_box_ascent: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the bottom - /// of the bounding rectangle used to render the text, in pixels. - pub actual_bounding_box_descent: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the top of - /// the em square in the line box, in pixels. - pub em_height_ascent: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the bottom - /// of the em square in the line box, in pixels. - pub em_height_descent: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the hanging - /// baseline of the line box, in pixels. - pub hanging_baseline: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the - /// alphabetic baseline of the line box, in pixels. - pub alphabetic_baseline: f32, - /// The distance from the horizontal line indicated by the `text_baseline` state to the - /// ideographic baseline of the line box, in pixels. - pub ideographic_baseline: f32, +// Avoids leaking `State` to the outside. +#[doc(hidden)] +pub struct CanvasState<'a>(&'a State); + +/// A trait that encompasses both text that has been laid out (i.e. `TextMetrics` or skribo's +/// `Layout`) and text that has not yet been laid out. +pub trait ToTextLayout { + #[doc(hidden)] + fn layout(&self, state: CanvasState) -> Cow; +} + +impl ToTextLayout for str { + fn layout(&self, state: CanvasState) -> Cow { + let skribo_layout = Rc::new(skribo::layout(&TextStyle { size: state.0.font_size }, + &state.0.font_collection, + self)); + Cow::Owned(TextMetrics::new(skribo_layout, + state.0.font_size, + state.0.text_align, + state.0.text_baseline)) + } +} + +impl ToTextLayout for String { + fn layout(&self, state: CanvasState) -> Cow { + let this: &str = self; + this.layout(state) + } +} + +impl ToTextLayout for Rc { + fn layout(&self, state: CanvasState) -> Cow { + Cow::Owned(TextMetrics::new((*self).clone(), + state.0.font_size, + state.0.text_align, + state.0.text_baseline)) + } +} + +impl ToTextLayout for TextMetrics { + fn layout(&self, _: CanvasState) -> Cow { + Cow::Borrowed(self) + } } #[cfg(feature = "pf-text")] @@ -249,75 +239,266 @@ impl CanvasFontContext { // Text layout utilities +/// A laid-out run of text. Text metrics can be queried from this structure, or it can be directly +/// passed into `fill_text()` and/or `stroke_text()` to draw the text without having to lay it out +/// again. +/// +/// Internally, this structure caches most of its layout queries. +#[derive(Clone)] +pub struct TextMetrics { + skribo_layout: Rc, + font_size: f32, + align: TextAlign, + baseline: TextBaseline, + text_x_offset: Cell>, + text_y_offset: Cell>, + vertical_metrics: Cell>, + // The calculated width of a segment of inline text in pixels. + width: Cell>, + // The distance from the typographic left side of the text to the left side of the bounding + // rectangle of the given text, in pixels. The distance is measured parallel to the baseline. + actual_left_extent: Cell>, + // The distance from the typographic right side of the text to the right side of the bounding + // rectangle of the given text, in pixels. The distance is measured parallel to the baseline. + actual_right_extent: Cell>, +} + +#[derive(Clone, Copy)] +struct VerticalMetrics { + // The distance from the horizontal line indicated by the `text_baseline` state to the top of + // the highest bounding rectangle of all the fonts used to render the text, in pixels. + font_bounding_box_ascent: f32, + // The distance from the horizontal line indicated by the `text_baseline` state to the bottom + // of the highest bounding rectangle of all the fonts used to render the text, in pixels. + font_bounding_box_descent: f32, + // The distance from the horizontal line indicated by the `text_baseline` state to the top of + // the bounding rectangle used to render the text, in pixels. + actual_bounding_box_ascent: f32, + // The distance from the horizontal line indicated by the `text_baseline` state to the bottom + // of the bounding rectangle used to render the text, in pixels. + actual_bounding_box_descent: f32, + // The distance from the horizontal line indicated by the `text_baseline` state to the top of + // the em square in the line box, in pixels. + em_height_ascent: f32, + // The distance from the horizontal line indicated by the `text_baseline` state to the bottom + // of the em square in the line box, in pixels. + em_height_descent: f32, + // The distance from the horizontal line indicated by the `text_baseline` state to the hanging + // baseline of the line box, in pixels. + hanging_baseline: f32, + // The distance from the horizontal line indicated by the `text_baseline` state to the + // alphabetic baseline of the line box, in pixels. + alphabetic_baseline: f32, + // The distance from the horizontal line indicated by the `text_baseline` state to the + // ideographic baseline of the line box, in pixels. + ideographic_baseline: f32, +} + impl TextMetrics { - fn text_origin(&self, state: &State) -> Vector2F { - let x = match state.text_align { - TextAlign::Left => 0.0, - TextAlign::Right => -self.width, - TextAlign::Center => -0.5 * self.width, - }; - - let y = match state.text_baseline { - TextBaseline::Alphabetic => 0.0, - TextBaseline::Top => self.em_height_ascent, - TextBaseline::Middle => util::lerp(self.em_height_ascent, self.em_height_descent, 0.5), - TextBaseline::Bottom => self.em_height_descent, - TextBaseline::Ideographic => self.ideographic_baseline, - TextBaseline::Hanging => self.hanging_baseline, - }; - - vec2f(x, y) + pub fn new(skribo_layout: Rc, + font_size: f32, + align: TextAlign, + baseline: TextBaseline) + -> TextMetrics { + TextMetrics { + skribo_layout, + font_size, + align, + baseline, + text_x_offset: Cell::new(None), + text_y_offset: Cell::new(None), + vertical_metrics: Cell::new(None), + width: Cell::new(None), + actual_left_extent: Cell::new(None), + actual_right_extent: Cell::new(None), + } } - fn make_origin_relative(&mut self, state: &State) { - let text_origin = self.text_origin(state); - self.actual_bounding_box_left += text_origin.x(); - self.actual_bounding_box_right += text_origin.x(); - self.font_bounding_box_ascent -= text_origin.y(); - self.font_bounding_box_descent -= text_origin.y(); - self.actual_bounding_box_ascent -= text_origin.y(); - self.actual_bounding_box_descent -= text_origin.y(); - self.em_height_ascent -= text_origin.y(); - self.em_height_descent -= text_origin.y(); - self.hanging_baseline -= text_origin.y(); - self.alphabetic_baseline -= text_origin.y(); - self.ideographic_baseline -= text_origin.y(); + pub fn text_x_offset(&self) -> f32 { + if self.text_x_offset.get().is_none() { + self.text_x_offset.set(Some(match self.align { + TextAlign::Left => 0.0, + TextAlign::Right => -self.width(), + TextAlign::Center => -0.5 * self.width(), + })); + } + self.text_x_offset.get().unwrap() } + + pub fn text_y_offset(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + if self.text_y_offset.get().is_none() { + let vertical_metrics = self.vertical_metrics.get().unwrap(); + self.text_y_offset.set(Some(match self.baseline { + TextBaseline::Alphabetic => 0.0, + TextBaseline::Top => vertical_metrics.em_height_ascent, + TextBaseline::Middle => { + util::lerp(vertical_metrics.em_height_ascent, + vertical_metrics.em_height_descent, + 0.5) + } + TextBaseline::Bottom => vertical_metrics.em_height_descent, + TextBaseline::Ideographic => vertical_metrics.ideographic_baseline, + TextBaseline::Hanging => vertical_metrics.hanging_baseline, + })); + } + self.text_y_offset.get().unwrap() + } + + fn text_origin(&self) -> Vector2F { + vec2f(self.text_x_offset(), self.text_y_offset()) + } + + pub fn width(&self) -> f32 { + if self.width.get().is_none() { + match self.skribo_layout.glyphs.last() { + None => self.width.set(Some(0.0)), + Some(last_glyph) => { + let glyph_id = last_glyph.glyph_id; + let font_metrics = last_glyph.font.font.metrics(); + let scale_factor = self.skribo_layout.size / font_metrics.units_per_em as f32; + let glyph_rect = last_glyph.font.font.typographic_bounds(glyph_id).unwrap(); + self.width.set(Some(last_glyph.offset.x() + + glyph_rect.max_x() * scale_factor)); + } + } + + } + self.width.get().unwrap() + } + + fn populate_vertical_metrics_if_necessary(&self) { + if self.vertical_metrics.get().is_none() { + self.vertical_metrics.set(Some(VerticalMetrics::measure(&self.skribo_layout))); + } + } + + pub fn font_bounding_box_ascent(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().font_bounding_box_ascent - self.text_y_offset() + } + + pub fn font_bounding_box_descent(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().font_bounding_box_descent - self.text_y_offset() + } + + pub fn actual_bounding_box_ascent(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().actual_bounding_box_ascent - self.text_y_offset() + } + + pub fn actual_bounding_box_descent(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().actual_bounding_box_descent - self.text_y_offset() + } + + pub fn em_height_ascent(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().em_height_ascent - self.text_y_offset() + } + + pub fn em_height_descent(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().em_height_descent - self.text_y_offset() + } + + pub fn actual_bounding_box_left(&self) -> f32 { + if self.actual_left_extent.get().is_none() { + match self.skribo_layout.glyphs.get(0) { + None => self.actual_left_extent.set(Some(0.0)), + Some(first_glyph) => { + let glyph_id = first_glyph.glyph_id; + let font_metrics = first_glyph.font.font.metrics(); + let scale_factor = self.skribo_layout.size / font_metrics.units_per_em as f32; + let glyph_rect = first_glyph.font.font.raster_bounds( + glyph_id, + font_metrics.units_per_em as f32, + Transform2F::default(), + HintingOptions::None, + RasterizationOptions::GrayscaleAa).unwrap(); + self.actual_left_extent.set(Some(first_glyph.offset.x() + + glyph_rect.min_x() as f32 * scale_factor)); + } + } + } + self.actual_left_extent.get().unwrap() + self.text_x_offset() + } + + pub fn actual_bounding_box_right(&self) -> f32 { + if self.actual_right_extent.get().is_none() { + match self.skribo_layout.glyphs.last() { + None => self.actual_right_extent.set(Some(0.0)), + Some(last_glyph) => { + let glyph_id = last_glyph.glyph_id; + let font_metrics = last_glyph.font.font.metrics(); + let scale_factor = self.skribo_layout.size / font_metrics.units_per_em as f32; + let glyph_rect = last_glyph.font.font.raster_bounds( + glyph_id, + font_metrics.units_per_em as f32, + Transform2F::default(), + HintingOptions::None, + RasterizationOptions::GrayscaleAa).unwrap(); + self.actual_right_extent.set(Some(last_glyph.offset.x() + + glyph_rect.max_x() as f32 * scale_factor)); + } + } + } + self.actual_right_extent.get().unwrap() + self.text_x_offset() + } + + pub fn hanging_baseline(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().hanging_baseline - self.text_y_offset() + } + + pub fn alphabetic_baseline(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().alphabetic_baseline - self.text_y_offset() + } + + pub fn ideographic_baseline(&self) -> f32 { + self.populate_vertical_metrics_if_necessary(); + self.vertical_metrics.get().unwrap().ideographic_baseline - self.text_y_offset() + } + } -pub trait LayoutExt { - fn metrics(&self) -> TextMetrics; - fn width(&self) -> f32; - fn actual_bounding_box_left(&self) -> f32; - fn actual_bounding_box_right(&self) -> f32; - fn hanging_baseline(&self) -> f32; - fn ideographic_baseline(&self) -> f32; -} - -impl LayoutExt for Layout { - // NB: This does not return origin-relative values. To get those, call `make_origin_relative()` - // afterward. - fn metrics(&self) -> TextMetrics { - let (mut em_height_ascent, mut em_height_descent) = (0.0, 0.0); - let (mut font_bounding_box_ascent, mut font_bounding_box_descent) = (0.0, 0.0); - let (mut actual_bounding_box_ascent, mut actual_bounding_box_descent) = (0.0, 0.0); +impl VerticalMetrics { + fn measure(skribo_layout: &SkriboLayout) -> VerticalMetrics { + let mut vertical_metrics = VerticalMetrics { + font_bounding_box_ascent: 0.0, + font_bounding_box_descent: 0.0, + actual_bounding_box_ascent: 0.0, + actual_bounding_box_descent: 0.0, + em_height_ascent: 0.0, + em_height_descent: 0.0, + hanging_baseline: 0.0, + alphabetic_baseline: 0.0, + ideographic_baseline: 0.0, + }; let mut last_font: Option> = None; - for glyph in &self.glyphs { + for glyph in &skribo_layout.glyphs { match last_font { Some(ref last_font) if Arc::ptr_eq(&last_font, &glyph.font.font) => {} _ => { let font = glyph.font.font.clone(); let font_metrics = font.metrics(); - let scale_factor = self.size / font_metrics.units_per_em as f32; - em_height_ascent = (font_metrics.ascent * scale_factor).max(em_height_ascent); - em_height_descent = - (font_metrics.descent * scale_factor).min(em_height_descent); - font_bounding_box_ascent = (font_metrics.bounding_box.max_y() * - scale_factor).max(font_bounding_box_ascent); - font_bounding_box_descent = (font_metrics.bounding_box.min_y() * - scale_factor).min(font_bounding_box_descent); + let scale_factor = skribo_layout.size / font_metrics.units_per_em as f32; + vertical_metrics.em_height_ascent = + (font_metrics.ascent * + scale_factor).max(vertical_metrics.em_height_ascent); + vertical_metrics.em_height_descent = + (font_metrics.descent * + scale_factor).min(vertical_metrics.em_height_descent); + vertical_metrics.font_bounding_box_ascent = + (font_metrics.bounding_box.max_y() * + scale_factor).max(vertical_metrics.font_bounding_box_ascent); + vertical_metrics.font_bounding_box_descent = + (font_metrics.bounding_box.min_y() * + scale_factor).min(vertical_metrics.font_bounding_box_descent); last_font = Some(font); } @@ -325,91 +506,17 @@ impl LayoutExt for Layout { let font = last_font.as_ref().unwrap(); let glyph_rect = font.raster_bounds(glyph.glyph_id, - self.size, + skribo_layout.size, Transform2F::default(), HintingOptions::None, RasterizationOptions::GrayscaleAa).unwrap(); - actual_bounding_box_ascent = - (glyph_rect.max_y() as f32).max(actual_bounding_box_ascent); - actual_bounding_box_descent = - (glyph_rect.min_y() as f32).min(actual_bounding_box_descent); + vertical_metrics.actual_bounding_box_ascent = + (glyph_rect.max_y() as f32).max(vertical_metrics.actual_bounding_box_ascent); + vertical_metrics.actual_bounding_box_descent = + (glyph_rect.min_y() as f32).min(vertical_metrics.actual_bounding_box_descent); } - TextMetrics { - width: self.width(), - actual_bounding_box_left: self.actual_bounding_box_left(), - actual_bounding_box_right: self.actual_bounding_box_right(), - font_bounding_box_ascent, - font_bounding_box_descent, - actual_bounding_box_ascent, - actual_bounding_box_descent, - em_height_ascent, - em_height_descent, - alphabetic_baseline: 0.0, - hanging_baseline: self.hanging_baseline(), - ideographic_baseline: self.ideographic_baseline(), - } - } - - fn width(&self) -> f32 { - let last_glyph = match self.glyphs.last() { - None => return 0.0, - Some(last_glyph) => last_glyph, - }; - - let glyph_id = last_glyph.glyph_id; - let font_metrics = last_glyph.font.font.metrics(); - let scale_factor = self.size / font_metrics.units_per_em as f32; - let glyph_rect = last_glyph.font.font.typographic_bounds(glyph_id).unwrap(); - last_glyph.offset.x() + glyph_rect.max_x() * scale_factor - } - - fn actual_bounding_box_left(&self) -> f32 { - let first_glyph = match self.glyphs.get(0) { - None => return 0.0, - Some(first_glyph) => first_glyph, - }; - - let glyph_id = first_glyph.glyph_id; - let font_metrics = first_glyph.font.font.metrics(); - let scale_factor = self.size / font_metrics.units_per_em as f32; - let glyph_rect = first_glyph.font - .font - .raster_bounds(glyph_id, - font_metrics.units_per_em as f32, - Transform2F::default(), - HintingOptions::None, - RasterizationOptions::GrayscaleAa).unwrap(); - first_glyph.offset.x() + glyph_rect.min_x() as f32 * scale_factor - } - - fn actual_bounding_box_right(&self) -> f32 { - let last_glyph = match self.glyphs.last() { - None => return 0.0, - Some(last_glyph) => last_glyph, - }; - - let glyph_id = last_glyph.glyph_id; - let font_metrics = last_glyph.font.font.metrics(); - let scale_factor = self.size / font_metrics.units_per_em as f32; - let glyph_rect = last_glyph.font - .font - .raster_bounds(glyph_id, - font_metrics.units_per_em as f32, - Transform2F::default(), - HintingOptions::None, - RasterizationOptions::GrayscaleAa).unwrap(); - last_glyph.offset.x() + glyph_rect.max_x() as f32 * scale_factor - } - - fn hanging_baseline(&self) -> f32 { - // TODO(pcwalton) - 0.0 - } - - fn ideographic_baseline(&self) -> f32 { - // TODO(pcwalton) - 0.0 + vertical_metrics } } diff --git a/examples/canvas_nanovg/src/main.rs b/examples/canvas_nanovg/src/main.rs index c01cb7d3..46f54df5 100644 --- a/examples/canvas_nanovg/src/main.rs +++ b/examples/canvas_nanovg/src/main.rs @@ -14,7 +14,7 @@ use font_kit::handle::Handle; use font_kit::sources::mem::MemSource; use image; use pathfinder_canvas::{Canvas, CanvasFontContext, CanvasRenderingContext2D, LineJoin, Path2D}; -use pathfinder_canvas::{TextAlign, TextBaseline}; +use pathfinder_canvas::{TextAlign, TextBaseline, TextMetrics}; use pathfinder_color::{ColorF, ColorU, rgbau, rgbf, rgbu}; use pathfinder_content::fill::FillRule; use pathfinder_content::gradient::Gradient; @@ -288,10 +288,10 @@ fn draw_paragraph(context: &mut CanvasRenderingContext2D, let gutter_text_metrics = context.measure_text(&gutter_text); let gutter_text_bounds = - RectF::from_points(vec2f(gutter_text_metrics.actual_bounding_box_left, - -gutter_text_metrics.font_bounding_box_ascent), - vec2f(gutter_text_metrics.actual_bounding_box_right, - -gutter_text_metrics.font_bounding_box_descent)); + RectF::from_points(vec2f(gutter_text_metrics.actual_bounding_box_left(), + -gutter_text_metrics.font_bounding_box_ascent()), + vec2f(gutter_text_metrics.actual_bounding_box_right(), + -gutter_text_metrics.font_bounding_box_descent())); let gutter_path_bounds = gutter_text_bounds.dilate(vec2f(4.0, 2.0)); let gutter_path_radius = gutter_path_bounds.width() * 0.5 - 1.0; let path = create_rounded_rect_path(gutter_path_bounds + gutter_origin, @@ -345,7 +345,8 @@ struct Line { } struct Word { - text: String, + metrics: TextMetrics, + string: String, origin_x: f32, } @@ -370,8 +371,8 @@ impl MultilineTextBox { const LINE_SPACING: f32 = 3.0; let a_b_measure = context.measure_text("A B"); - let space_width = a_b_measure.width - context.measure_text("AB").width; - let line_height = a_b_measure.em_height_ascent - a_b_measure.em_height_descent + + let space_width = a_b_measure.width() - context.measure_text("AB").width(); + let line_height = a_b_measure.em_height_ascent() - a_b_measure.em_height_descent() + LINE_SPACING; let mut text: VecDeque> = text.split('\n').map(|paragraph| { @@ -440,16 +441,16 @@ impl Line { } let word_metrics = context.measure_text(&word); - let new_line_width = word_origin_x + word_metrics.width; + let new_line_width = word_origin_x + word_metrics.width(); if self.width != 0.0 && new_line_width > self.max_width { text.push_front(word); return; } - self.words.push(Word { text: word, origin_x: word_origin_x }); + self.ascent = self.ascent.max(word_metrics.em_height_ascent()); + self.descent = self.descent.min(word_metrics.em_height_descent()); + self.words.push(Word { metrics: word_metrics, string: word, origin_x: word_origin_x }); self.width = new_line_width; - self.ascent = self.ascent.max(word_metrics.em_height_ascent); - self.descent = self.descent.min(word_metrics.em_height_descent); } } @@ -464,7 +465,7 @@ impl Line { context.set_fill_style(fg_color); for word in &self.words { - context.fill_text(&word.text, self.origin + vec2f(word.origin_x, 0.0)); + context.fill_text(&word.metrics, self.origin + vec2f(word.origin_x, 0.0)); } } @@ -504,23 +505,24 @@ impl Line { impl Word { fn hit_test(&self, context: &CanvasRenderingContext2D, position_x: f32) -> u32 { let (mut char_start_x, mut prev_char_index) = (self.origin_x, 0); - for char_index in self.text + for char_index in self.string .char_indices() .map(|(index, _)| index) .skip(1) - .chain(iter::once(self.text.len())) { - let char_end_x = self.origin_x + context.measure_text(&self.text[0..char_index]).width; + .chain(iter::once(self.string.len())) { + let char_end_x = self.origin_x + + context.measure_text(&self.string[0..char_index]).width(); if position_x <= (char_start_x + char_end_x) * 0.5 { return prev_char_index; } char_start_x = char_end_x; prev_char_index = char_index as u32; } - return self.text.len() as u32; + return self.string.len() as u32; } fn char_position(&self, context: &CanvasRenderingContext2D, char_index: u32) -> f32 { - context.measure_text(&self.text[0..(char_index as usize)]).width + context.measure_text(&self.string[0..(char_index as usize)]).width() } } @@ -996,7 +998,7 @@ fn draw_numeric_edit_box(context: &mut CanvasRenderingContext2D, context.set_font(FONT_NAME_REGULAR); context.set_font_size(15.0); - let unit_width = context.measure_text(unit).width; + let unit_width = context.measure_text(unit).width(); context.set_fill_style(rgbau(255, 255, 255, 64)); context.set_text_align(TextAlign::Right); @@ -1071,7 +1073,7 @@ fn draw_button(context: &mut CanvasRenderingContext2D, context.set_font(FONT_NAME_BOLD); context.set_font_size(17.0); - let text_width = context.measure_text(text).width; + let text_width = context.measure_text(text).width(); let icon_width; match pre_icon { @@ -1079,7 +1081,7 @@ fn draw_button(context: &mut CanvasRenderingContext2D, Some(icon) => { context.set_font_size(rect.height() * 0.7); context.set_font(FONT_NAME_EMOJI); - icon_width = context.measure_text(icon).width + rect.height() * 0.15; + icon_width = context.measure_text(icon).width() + rect.height() * 0.15; context.set_fill_style(rgbau(255, 255, 255, 96)); context.set_text_align(TextAlign::Left); context.set_text_baseline(TextBaseline::Middle); diff --git a/text/src/lib.rs b/text/src/lib.rs index c4300987..b3e6cf24 100644 --- a/text/src/lib.rs +++ b/text/src/lib.rs @@ -12,6 +12,7 @@ use font_kit::error::GlyphLoadingError; use font_kit::hinting::HintingOptions; use font_kit::loader::Loader; use font_kit::loaders::default::Font as DefaultLoader; +use font_kit::metrics::Metrics; use font_kit::outline::OutlineSink; use pathfinder_content::effects::BlendMode; use pathfinder_content::outline::{Contour, Outline}; @@ -24,6 +25,7 @@ use pathfinder_renderer::scene::{ClipPathId, DrawPath, Scene}; use skribo::{FontCollection, Layout, TextStyle}; use std::collections::HashMap; use std::mem; +use std::sync::Arc; #[derive(Clone)] pub struct FontContext where F: Loader { @@ -33,6 +35,7 @@ pub struct FontContext where F: Loader { #[derive(Clone)] struct FontInfo where F: Loader { font: F, + metrics: Metrics, outline_cache: HashMap, } @@ -60,6 +63,11 @@ impl Default for FontRenderOptions { } } +enum FontInfoRefMut<'a, F> where F: Loader { + Ref(&'a mut FontInfo), + Owned(FontInfo), +} + #[derive(Clone, Copy, PartialEq, Debug, Eq, Hash)] pub struct GlyphId(pub u32); @@ -69,59 +77,66 @@ impl FontContext where F: Loader { FontContext { font_info: HashMap::new() } } - pub fn push_glyph(&mut self, - scene: &mut Scene, - font: &F, - glyph_id: GlyphId, - render_options: &FontRenderOptions) - -> Result<(), GlyphLoadingError> { - let font_key = font.postscript_name(); - let metrics = font.metrics(); - + fn push_glyph(&mut self, + scene: &mut Scene, + font: &F, + font_key: Option<&str>, + glyph_id: GlyphId, + glyph_offset: Vector2F, + font_size: f32, + render_options: &FontRenderOptions) + -> Result<(), GlyphLoadingError> { // Insert the font into the cache if needed. - if let Some(ref font_key) = font_key { - if !self.font_info.contains_key(&*font_key) { - self.font_info.insert((*font_key).clone(), FontInfo::new((*font).clone())); + let mut font_info = match font_key { + Some(font_key) => { + if !self.font_info.contains_key(&*font_key) { + self.font_info.insert(font_key.to_owned(), FontInfo::new((*font).clone())); + } + FontInfoRefMut::Ref(self.font_info.get_mut(&*font_key).unwrap()) } - } + None => { + // FIXME(pcwalton): This slow path can be removed once we have a unique font ID in + // `font-kit`. + FontInfoRefMut::Owned(FontInfo::new((*font).clone())) + } + }; + let font_info = font_info.get_mut(); // See if we have a cached outline. // // TODO(pcwalton): Cache hinted outlines too. let mut cached_outline = None; - let can_cache_outline = font_key.is_some() && - render_options.hinting_options == HintingOptions::None; + let can_cache_outline = render_options.hinting_options == HintingOptions::None; if can_cache_outline { - if let Some(ref font_info) = self.font_info.get(&*font_key.as_ref().unwrap()) { - if let Some(ref outline) = font_info.outline_cache.get(&glyph_id) { - cached_outline = Some((*outline).clone()); - } + if let Some(ref outline) = font_info.outline_cache.get(&glyph_id) { + cached_outline = Some((*outline).clone()); } } + let metrics = &font_info.metrics; + let font_scale = font_size / metrics.units_per_em as f32; + let render_transform = render_options.transform * + Transform2F::from_scale(vec2f(font_scale, -font_scale)).translate(glyph_offset); + let mut outline = match cached_outline { Some(mut cached_outline) => { let scale = 1.0 / metrics.units_per_em as f32; - cached_outline.transform(&(render_options.transform * - Transform2F::from_scale(scale))); + cached_outline.transform(&(render_transform * Transform2F::from_scale(scale))); cached_outline } None => { let transform = if can_cache_outline { Transform2F::from_scale(metrics.units_per_em as f32) } else { - render_options.transform + render_transform }; let mut outline_builder = OutlinePathBuilder::new(&transform); font.outline(glyph_id.0, render_options.hinting_options, &mut outline_builder)?; let mut outline = outline_builder.build(); if can_cache_outline { - let font_key = font_key.as_ref().unwrap(); - let font_info = self.font_info.get_mut(&*font_key).unwrap(); font_info.outline_cache.insert(glyph_id, outline.clone()); let scale = 1.0 / metrics.units_per_em as f32; - outline.transform(&(render_options.transform * - Transform2F::from_scale(scale))); + outline.transform(&(render_transform * Transform2F::from_scale(scale))); } outline } @@ -155,18 +170,26 @@ impl FontContext { style: &TextStyle, render_options: &FontRenderOptions) -> Result<(), GlyphLoadingError> { + let mut cached_font_key: Option> = None; for glyph in &layout.glyphs { - let offset = glyph.offset; - let font = &*glyph.font.font; - // FIXME(pcwalton): Cache this! - let scale = style.size / (font.metrics().units_per_em as f32); - let scale = vec2f(scale, -scale); - let render_options = FontRenderOptions { - transform: render_options.transform * - Transform2F::from_scale(scale).translate(offset), - ..*render_options - }; - self.push_glyph(scene, font, GlyphId(glyph.glyph_id), &render_options)?; + match cached_font_key { + Some(ref cached_font_key) if Arc::ptr_eq(&cached_font_key.font, + &glyph.font.font) => {} + _ => { + cached_font_key = Some(CachedFontKey { + font: glyph.font.font.clone(), + key: glyph.font.font.postscript_name(), + }); + } + } + let cached_font_key = cached_font_key.as_ref().unwrap(); + self.push_glyph(scene, + &*cached_font_key.font, + cached_font_key.key.as_ref().map(|key| &**key), + GlyphId(glyph.glyph_id), + glyph.offset, + style.size, + &render_options)?; } Ok(()) } @@ -184,9 +207,24 @@ impl FontContext { } } +struct CachedFontKey where F: Loader { + font: Arc, + key: Option, +} + impl FontInfo where F: Loader { fn new(font: F) -> FontInfo { - FontInfo { font, outline_cache: HashMap::new() } + let metrics = font.metrics(); + FontInfo { font, metrics, outline_cache: HashMap::new() } + } +} + +impl<'a, F> FontInfoRefMut<'a, F> where F: Loader { + fn get_mut(&mut self) -> &mut FontInfo { + match *self { + FontInfoRefMut::Ref(ref mut reference) => &mut **reference, + FontInfoRefMut::Owned(ref mut info) => info, + } } }