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.
This commit is contained in:
Patrick Walton 2020-07-17 12:39:42 -07:00
parent 448ede2b1f
commit d01bc5d002
4 changed files with 431 additions and 284 deletions

View File

@ -826,7 +826,7 @@ trait TextMetricsExt {
impl TextMetricsExt for TextMetrics { impl TextMetricsExt for TextMetrics {
fn to_c(&self) -> PFTextMetrics { fn to_c(&self) -> PFTextMetrics {
PFTextMetrics { width: self.width } PFTextMetrics { width: self.width() }
} }
} }

View File

@ -22,66 +22,59 @@ use pathfinder_geometry::util;
use pathfinder_geometry::vector::{Vector2F, vec2f}; use pathfinder_geometry::vector::{Vector2F, vec2f};
use pathfinder_renderer::paint::PaintId; use pathfinder_renderer::paint::PaintId;
use pathfinder_text::{FontContext, FontRenderOptions, TextRenderMode}; use pathfinder_text::{FontContext, FontRenderOptions, TextRenderMode};
use skribo::{FontCollection, FontFamily, FontRef, Layout, TextStyle}; use skribo::{FontCollection, FontFamily, FontRef, Layout as SkriboLayout, TextStyle};
use std::cell::RefCell; use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
impl CanvasRenderingContext2D { 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<T>(&mut self, text: &T, position: Vector2F) where T: ToTextLayout + ?Sized {
let paint = self.current_state.resolve_paint(&self.current_state.fill_paint); let paint = self.current_state.resolve_paint(&self.current_state.fill_paint);
let paint_id = self.canvas.scene.push_paint(&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<T>(&mut self, text: &T, position: Vector2F) where T: ToTextLayout + ?Sized {
let paint = self.current_state.resolve_paint(&self.current_state.stroke_paint); let paint = self.current_state.resolve_paint(&self.current_state.stroke_paint);
let paint_id = self.canvas.scene.push_paint(&paint); let paint_id = self.canvas.scene.push_paint(&paint);
let render_mode = TextRenderMode::Stroke(self.current_state.resolve_stroke_style()); 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 { /// Returns metrics of the given text using the current style.
let mut metrics = self.layout_text(string).metrics(); ///
metrics.make_origin_relative(&self.current_state); /// As an extension, the returned `TextMetrics` object contains all the layout data for the
metrics /// 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<T>(&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) { fn fill_or_stroke_text<T>(&mut self,
let paint_id = self.canvas.scene.push_paint(&self.current_state.fill_paint); 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 clip_path = self.current_state.clip_path;
let blend_mode = self.current_state.global_composite_operation.to_blend_mode(); let blend_mode = self.current_state.global_composite_operation.to_blend_mode();
// TODO(pcwalton): Report errors. position += layout.text_origin();
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);
let transform = self.current_state.transform * Transform2F::from_translation(position); let transform = self.current_state.transform * Transform2F::from_translation(position);
// TODO(pcwalton): Report errors. // TODO(pcwalton): Report errors.
@ -90,8 +83,8 @@ impl CanvasRenderingContext2D {
.borrow_mut() .borrow_mut()
.font_context .font_context
.push_layout(&mut self.canvas.scene, .push_layout(&mut self.canvas.scene,
&layout, &layout.skribo_layout,
&TextStyle { size: self.current_state.font_size }, &TextStyle { size: layout.font_size },
&FontRenderOptions { &FontRenderOptions {
transform, transform,
render_mode, 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 // Text styles
#[inline] #[inline]
@ -152,46 +139,49 @@ impl CanvasRenderingContext2D {
} }
} }
/// Represents the dimensions of a piece of text in the canvas. // Avoids leaking `State` to the outside.
#[derive(Clone, Copy, Debug)] #[doc(hidden)]
pub struct TextMetrics { pub struct CanvasState<'a>(&'a State);
/// The calculated width of a segment of inline text in pixels.
pub width: f32, /// A trait that encompasses both text that has been laid out (i.e. `TextMetrics` or skribo's
/// The distance from the alignment point given by the `text_align` state to the left side of /// `Layout`) and text that has not yet been laid out.
/// the bounding rectangle of the given text, in pixels. The distance is measured parallel to pub trait ToTextLayout {
/// the baseline. #[doc(hidden)]
pub actual_bounding_box_left: f32, fn layout(&self, state: CanvasState) -> Cow<TextMetrics>;
/// 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. impl ToTextLayout for str {
pub actual_bounding_box_right: f32, fn layout(&self, state: CanvasState) -> Cow<TextMetrics> {
/// The distance from the horizontal line indicated by the `text_baseline` state to the top of let skribo_layout = Rc::new(skribo::layout(&TextStyle { size: state.0.font_size },
/// the highest bounding rectangle of all the fonts used to render the text, in pixels. &state.0.font_collection,
pub font_bounding_box_ascent: f32, self));
/// The distance from the horizontal line indicated by the `text_baseline` state to the bottom Cow::Owned(TextMetrics::new(skribo_layout,
/// of the highest bounding rectangle of all the fonts used to render the text, in pixels. state.0.font_size,
pub font_bounding_box_descent: f32, state.0.text_align,
/// The distance from the horizontal line indicated by the `text_baseline` state to the top of state.0.text_baseline))
/// 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. impl ToTextLayout for String {
pub actual_bounding_box_descent: f32, fn layout(&self, state: CanvasState) -> Cow<TextMetrics> {
/// The distance from the horizontal line indicated by the `text_baseline` state to the top of let this: &str = self;
/// the em square in the line box, in pixels. this.layout(state)
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, impl ToTextLayout for Rc<SkriboLayout> {
/// The distance from the horizontal line indicated by the `text_baseline` state to the hanging fn layout(&self, state: CanvasState) -> Cow<TextMetrics> {
/// baseline of the line box, in pixels. Cow::Owned(TextMetrics::new((*self).clone(),
pub hanging_baseline: f32, state.0.font_size,
/// The distance from the horizontal line indicated by the `text_baseline` state to the state.0.text_align,
/// alphabetic baseline of the line box, in pixels. state.0.text_baseline))
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, impl ToTextLayout for TextMetrics {
fn layout(&self, _: CanvasState) -> Cow<TextMetrics> {
Cow::Borrowed(self)
}
} }
#[cfg(feature = "pf-text")] #[cfg(feature = "pf-text")]
@ -249,75 +239,266 @@ impl CanvasFontContext {
// Text layout utilities // 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<SkriboLayout>,
font_size: f32,
align: TextAlign,
baseline: TextBaseline,
text_x_offset: Cell<Option<f32>>,
text_y_offset: Cell<Option<f32>>,
vertical_metrics: Cell<Option<VerticalMetrics>>,
// The calculated width of a segment of inline text in pixels.
width: Cell<Option<f32>>,
// 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<Option<f32>>,
// 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<Option<f32>>,
}
#[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 { impl TextMetrics {
fn text_origin(&self, state: &State) -> Vector2F { pub fn new(skribo_layout: Rc<SkriboLayout>,
let x = match state.text_align { font_size: f32,
TextAlign::Left => 0.0, align: TextAlign,
TextAlign::Right => -self.width, baseline: TextBaseline)
TextAlign::Center => -0.5 * self.width, -> TextMetrics {
}; TextMetrics {
skribo_layout,
let y = match state.text_baseline { font_size,
TextBaseline::Alphabetic => 0.0, align,
TextBaseline::Top => self.em_height_ascent, baseline,
TextBaseline::Middle => util::lerp(self.em_height_ascent, self.em_height_descent, 0.5), text_x_offset: Cell::new(None),
TextBaseline::Bottom => self.em_height_descent, text_y_offset: Cell::new(None),
TextBaseline::Ideographic => self.ideographic_baseline, vertical_metrics: Cell::new(None),
TextBaseline::Hanging => self.hanging_baseline, width: Cell::new(None),
}; actual_left_extent: Cell::new(None),
actual_right_extent: Cell::new(None),
vec2f(x, y) }
} }
fn make_origin_relative(&mut self, state: &State) { pub fn text_x_offset(&self) -> f32 {
let text_origin = self.text_origin(state); if self.text_x_offset.get().is_none() {
self.actual_bounding_box_left += text_origin.x(); self.text_x_offset.set(Some(match self.align {
self.actual_bounding_box_right += text_origin.x(); TextAlign::Left => 0.0,
self.font_bounding_box_ascent -= text_origin.y(); TextAlign::Right => -self.width(),
self.font_bounding_box_descent -= text_origin.y(); TextAlign::Center => -0.5 * self.width(),
self.actual_bounding_box_ascent -= text_origin.y(); }));
self.actual_bounding_box_descent -= text_origin.y(); }
self.em_height_ascent -= text_origin.y(); self.text_x_offset.get().unwrap()
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_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 { impl VerticalMetrics {
fn metrics(&self) -> TextMetrics; fn measure(skribo_layout: &SkriboLayout) -> VerticalMetrics {
fn width(&self) -> f32; let mut vertical_metrics = VerticalMetrics {
fn actual_bounding_box_left(&self) -> f32; font_bounding_box_ascent: 0.0,
fn actual_bounding_box_right(&self) -> f32; font_bounding_box_descent: 0.0,
fn hanging_baseline(&self) -> f32; actual_bounding_box_ascent: 0.0,
fn ideographic_baseline(&self) -> f32; actual_bounding_box_descent: 0.0,
} em_height_ascent: 0.0,
em_height_descent: 0.0,
impl LayoutExt for Layout { hanging_baseline: 0.0,
// NB: This does not return origin-relative values. To get those, call `make_origin_relative()` alphabetic_baseline: 0.0,
// afterward. ideographic_baseline: 0.0,
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);
let mut last_font: Option<Arc<Font>> = None; let mut last_font: Option<Arc<Font>> = None;
for glyph in &self.glyphs { for glyph in &skribo_layout.glyphs {
match last_font { match last_font {
Some(ref last_font) if Arc::ptr_eq(&last_font, &glyph.font.font) => {} Some(ref last_font) if Arc::ptr_eq(&last_font, &glyph.font.font) => {}
_ => { _ => {
let font = glyph.font.font.clone(); let font = glyph.font.font.clone();
let font_metrics = font.metrics(); let font_metrics = font.metrics();
let scale_factor = self.size / font_metrics.units_per_em as f32; let scale_factor = skribo_layout.size / font_metrics.units_per_em as f32;
em_height_ascent = (font_metrics.ascent * scale_factor).max(em_height_ascent); vertical_metrics.em_height_ascent =
em_height_descent = (font_metrics.ascent *
(font_metrics.descent * scale_factor).min(em_height_descent); scale_factor).max(vertical_metrics.em_height_ascent);
font_bounding_box_ascent = (font_metrics.bounding_box.max_y() * vertical_metrics.em_height_descent =
scale_factor).max(font_bounding_box_ascent); (font_metrics.descent *
font_bounding_box_descent = (font_metrics.bounding_box.min_y() * scale_factor).min(vertical_metrics.em_height_descent);
scale_factor).min(font_bounding_box_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); last_font = Some(font);
} }
@ -325,91 +506,17 @@ impl LayoutExt for Layout {
let font = last_font.as_ref().unwrap(); let font = last_font.as_ref().unwrap();
let glyph_rect = font.raster_bounds(glyph.glyph_id, let glyph_rect = font.raster_bounds(glyph.glyph_id,
self.size, skribo_layout.size,
Transform2F::default(), Transform2F::default(),
HintingOptions::None, HintingOptions::None,
RasterizationOptions::GrayscaleAa).unwrap(); RasterizationOptions::GrayscaleAa).unwrap();
actual_bounding_box_ascent = vertical_metrics.actual_bounding_box_ascent =
(glyph_rect.max_y() as f32).max(actual_bounding_box_ascent); (glyph_rect.max_y() as f32).max(vertical_metrics.actual_bounding_box_ascent);
actual_bounding_box_descent = vertical_metrics.actual_bounding_box_descent =
(glyph_rect.min_y() as f32).min(actual_bounding_box_descent); (glyph_rect.min_y() as f32).min(vertical_metrics.actual_bounding_box_descent);
} }
TextMetrics { vertical_metrics
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
} }
} }

View File

@ -14,7 +14,7 @@ use font_kit::handle::Handle;
use font_kit::sources::mem::MemSource; use font_kit::sources::mem::MemSource;
use image; use image;
use pathfinder_canvas::{Canvas, CanvasFontContext, CanvasRenderingContext2D, LineJoin, Path2D}; 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_color::{ColorF, ColorU, rgbau, rgbf, rgbu};
use pathfinder_content::fill::FillRule; use pathfinder_content::fill::FillRule;
use pathfinder_content::gradient::Gradient; 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_metrics = context.measure_text(&gutter_text);
let gutter_text_bounds = let gutter_text_bounds =
RectF::from_points(vec2f(gutter_text_metrics.actual_bounding_box_left, RectF::from_points(vec2f(gutter_text_metrics.actual_bounding_box_left(),
-gutter_text_metrics.font_bounding_box_ascent), -gutter_text_metrics.font_bounding_box_ascent()),
vec2f(gutter_text_metrics.actual_bounding_box_right, vec2f(gutter_text_metrics.actual_bounding_box_right(),
-gutter_text_metrics.font_bounding_box_descent)); -gutter_text_metrics.font_bounding_box_descent()));
let gutter_path_bounds = gutter_text_bounds.dilate(vec2f(4.0, 2.0)); 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 gutter_path_radius = gutter_path_bounds.width() * 0.5 - 1.0;
let path = create_rounded_rect_path(gutter_path_bounds + gutter_origin, let path = create_rounded_rect_path(gutter_path_bounds + gutter_origin,
@ -345,7 +345,8 @@ struct Line {
} }
struct Word { struct Word {
text: String, metrics: TextMetrics,
string: String,
origin_x: f32, origin_x: f32,
} }
@ -370,8 +371,8 @@ impl MultilineTextBox {
const LINE_SPACING: f32 = 3.0; const LINE_SPACING: f32 = 3.0;
let a_b_measure = context.measure_text("A B"); let a_b_measure = context.measure_text("A B");
let space_width = a_b_measure.width - context.measure_text("AB").width; 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 line_height = a_b_measure.em_height_ascent() - a_b_measure.em_height_descent() +
LINE_SPACING; LINE_SPACING;
let mut text: VecDeque<VecDeque<_>> = text.split('\n').map(|paragraph| { let mut text: VecDeque<VecDeque<_>> = text.split('\n').map(|paragraph| {
@ -440,16 +441,16 @@ impl Line {
} }
let word_metrics = context.measure_text(&word); 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 { if self.width != 0.0 && new_line_width > self.max_width {
text.push_front(word); text.push_front(word);
return; 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.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); context.set_fill_style(fg_color);
for word in &self.words { 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 { impl Word {
fn hit_test(&self, context: &CanvasRenderingContext2D, position_x: f32) -> u32 { fn hit_test(&self, context: &CanvasRenderingContext2D, position_x: f32) -> u32 {
let (mut char_start_x, mut prev_char_index) = (self.origin_x, 0); 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() .char_indices()
.map(|(index, _)| index) .map(|(index, _)| index)
.skip(1) .skip(1)
.chain(iter::once(self.text.len())) { .chain(iter::once(self.string.len())) {
let char_end_x = self.origin_x + context.measure_text(&self.text[0..char_index]).width; 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 { if position_x <= (char_start_x + char_end_x) * 0.5 {
return prev_char_index; return prev_char_index;
} }
char_start_x = char_end_x; char_start_x = char_end_x;
prev_char_index = char_index as u32; 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 { 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(FONT_NAME_REGULAR);
context.set_font_size(15.0); 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_fill_style(rgbau(255, 255, 255, 64));
context.set_text_align(TextAlign::Right); 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(FONT_NAME_BOLD);
context.set_font_size(17.0); 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; let icon_width;
match pre_icon { match pre_icon {
@ -1079,7 +1081,7 @@ fn draw_button(context: &mut CanvasRenderingContext2D,
Some(icon) => { Some(icon) => {
context.set_font_size(rect.height() * 0.7); context.set_font_size(rect.height() * 0.7);
context.set_font(FONT_NAME_EMOJI); 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_fill_style(rgbau(255, 255, 255, 96));
context.set_text_align(TextAlign::Left); context.set_text_align(TextAlign::Left);
context.set_text_baseline(TextBaseline::Middle); context.set_text_baseline(TextBaseline::Middle);

View File

@ -12,6 +12,7 @@ use font_kit::error::GlyphLoadingError;
use font_kit::hinting::HintingOptions; use font_kit::hinting::HintingOptions;
use font_kit::loader::Loader; use font_kit::loader::Loader;
use font_kit::loaders::default::Font as DefaultLoader; use font_kit::loaders::default::Font as DefaultLoader;
use font_kit::metrics::Metrics;
use font_kit::outline::OutlineSink; use font_kit::outline::OutlineSink;
use pathfinder_content::effects::BlendMode; use pathfinder_content::effects::BlendMode;
use pathfinder_content::outline::{Contour, Outline}; use pathfinder_content::outline::{Contour, Outline};
@ -24,6 +25,7 @@ use pathfinder_renderer::scene::{ClipPathId, DrawPath, Scene};
use skribo::{FontCollection, Layout, TextStyle}; use skribo::{FontCollection, Layout, TextStyle};
use std::collections::HashMap; use std::collections::HashMap;
use std::mem; use std::mem;
use std::sync::Arc;
#[derive(Clone)] #[derive(Clone)]
pub struct FontContext<F> where F: Loader { pub struct FontContext<F> where F: Loader {
@ -33,6 +35,7 @@ pub struct FontContext<F> where F: Loader {
#[derive(Clone)] #[derive(Clone)]
struct FontInfo<F> where F: Loader { struct FontInfo<F> where F: Loader {
font: F, font: F,
metrics: Metrics,
outline_cache: HashMap<GlyphId, Outline>, outline_cache: HashMap<GlyphId, Outline>,
} }
@ -60,6 +63,11 @@ impl Default for FontRenderOptions {
} }
} }
enum FontInfoRefMut<'a, F> where F: Loader {
Ref(&'a mut FontInfo<F>),
Owned(FontInfo<F>),
}
#[derive(Clone, Copy, PartialEq, Debug, Eq, Hash)] #[derive(Clone, Copy, PartialEq, Debug, Eq, Hash)]
pub struct GlyphId(pub u32); pub struct GlyphId(pub u32);
@ -69,59 +77,66 @@ impl<F> FontContext<F> where F: Loader {
FontContext { font_info: HashMap::new() } FontContext { font_info: HashMap::new() }
} }
pub fn push_glyph(&mut self, fn push_glyph(&mut self,
scene: &mut Scene, scene: &mut Scene,
font: &F, font: &F,
glyph_id: GlyphId, font_key: Option<&str>,
render_options: &FontRenderOptions) glyph_id: GlyphId,
-> Result<(), GlyphLoadingError> { glyph_offset: Vector2F,
let font_key = font.postscript_name(); font_size: f32,
let metrics = font.metrics(); render_options: &FontRenderOptions)
-> Result<(), GlyphLoadingError> {
// Insert the font into the cache if needed. // Insert the font into the cache if needed.
if let Some(ref font_key) = font_key { let mut font_info = match font_key {
if !self.font_info.contains_key(&*font_key) { Some(font_key) => {
self.font_info.insert((*font_key).clone(), FontInfo::new((*font).clone())); 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. // See if we have a cached outline.
// //
// TODO(pcwalton): Cache hinted outlines too. // TODO(pcwalton): Cache hinted outlines too.
let mut cached_outline = None; let mut cached_outline = None;
let can_cache_outline = font_key.is_some() && let can_cache_outline = render_options.hinting_options == HintingOptions::None;
render_options.hinting_options == HintingOptions::None;
if can_cache_outline { 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) {
if let Some(ref outline) = font_info.outline_cache.get(&glyph_id) { cached_outline = Some((*outline).clone());
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 { let mut outline = match cached_outline {
Some(mut cached_outline) => { Some(mut cached_outline) => {
let scale = 1.0 / metrics.units_per_em as f32; let scale = 1.0 / metrics.units_per_em as f32;
cached_outline.transform(&(render_options.transform * cached_outline.transform(&(render_transform * Transform2F::from_scale(scale)));
Transform2F::from_scale(scale)));
cached_outline cached_outline
} }
None => { None => {
let transform = if can_cache_outline { let transform = if can_cache_outline {
Transform2F::from_scale(metrics.units_per_em as f32) Transform2F::from_scale(metrics.units_per_em as f32)
} else { } else {
render_options.transform render_transform
}; };
let mut outline_builder = OutlinePathBuilder::new(&transform); let mut outline_builder = OutlinePathBuilder::new(&transform);
font.outline(glyph_id.0, render_options.hinting_options, &mut outline_builder)?; font.outline(glyph_id.0, render_options.hinting_options, &mut outline_builder)?;
let mut outline = outline_builder.build(); let mut outline = outline_builder.build();
if can_cache_outline { 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()); font_info.outline_cache.insert(glyph_id, outline.clone());
let scale = 1.0 / metrics.units_per_em as f32; let scale = 1.0 / metrics.units_per_em as f32;
outline.transform(&(render_options.transform * outline.transform(&(render_transform * Transform2F::from_scale(scale)));
Transform2F::from_scale(scale)));
} }
outline outline
} }
@ -155,18 +170,26 @@ impl FontContext<DefaultLoader> {
style: &TextStyle, style: &TextStyle,
render_options: &FontRenderOptions) render_options: &FontRenderOptions)
-> Result<(), GlyphLoadingError> { -> Result<(), GlyphLoadingError> {
let mut cached_font_key: Option<CachedFontKey<DefaultLoader>> = None;
for glyph in &layout.glyphs { for glyph in &layout.glyphs {
let offset = glyph.offset; match cached_font_key {
let font = &*glyph.font.font; Some(ref cached_font_key) if Arc::ptr_eq(&cached_font_key.font,
// FIXME(pcwalton): Cache this! &glyph.font.font) => {}
let scale = style.size / (font.metrics().units_per_em as f32); _ => {
let scale = vec2f(scale, -scale); cached_font_key = Some(CachedFontKey {
let render_options = FontRenderOptions { font: glyph.font.font.clone(),
transform: render_options.transform * key: glyph.font.font.postscript_name(),
Transform2F::from_scale(scale).translate(offset), });
..*render_options }
}; }
self.push_glyph(scene, font, GlyphId(glyph.glyph_id), &render_options)?; 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(()) Ok(())
} }
@ -184,9 +207,24 @@ impl FontContext<DefaultLoader> {
} }
} }
struct CachedFontKey<F> where F: Loader {
font: Arc<F>,
key: Option<String>,
}
impl<F> FontInfo<F> where F: Loader { impl<F> FontInfo<F> where F: Loader {
fn new(font: F) -> FontInfo<F> { fn new(font: F) -> FontInfo<F> {
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<F> {
match *self {
FontInfoRefMut::Ref(ref mut reference) => &mut **reference,
FontInfoRefMut::Owned(ref mut info) => info,
}
} }
} }