Auto merge of #408 - pcwalton:font-metadata-caching, r=pcwalton

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:
bors-servo 2020-07-21 22:33:37 -04:00 committed by GitHub
commit ca201ac0c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 431 additions and 284 deletions

View File

@ -826,7 +826,7 @@ trait TextMetricsExt {
impl TextMetricsExt for TextMetrics {
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_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<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_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_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<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) {
let paint_id = self.canvas.scene.push_paint(&self.current_state.fill_paint);
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,
fn fill_or_stroke_text<T>(&mut self,
text: &T,
mut position: Vector2F,
paint_id: PaintId,
render_mode: TextRenderMode) {
let layout = self.layout_text(string);
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();
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<TextMetrics>;
}
impl ToTextLayout for str {
fn layout(&self, state: CanvasState) -> Cow<TextMetrics> {
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<TextMetrics> {
let this: &str = self;
this.layout(state)
}
}
impl ToTextLayout for Rc<SkriboLayout> {
fn layout(&self, state: CanvasState) -> Cow<TextMetrics> {
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<TextMetrics> {
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<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 {
fn text_origin(&self, state: &State) -> Vector2F {
let x = match state.text_align {
pub fn new(skribo_layout: Rc<SkriboLayout>,
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),
}
}
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,
};
TextAlign::Right => -self.width(),
TextAlign::Center => -0.5 * self.width(),
}));
}
self.text_x_offset.get().unwrap()
}
let y = match state.text_baseline {
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 => 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,
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()
}
}
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,
};
vec2f(x, y)
}
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 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);
let mut last_font: Option<Arc<Font>> = 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
}
}

View File

@ -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<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);

View File

@ -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<F> where F: Loader {
@ -33,6 +35,7 @@ pub struct FontContext<F> where F: Loader {
#[derive(Clone)]
struct FontInfo<F> where F: Loader {
font: F,
metrics: Metrics,
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)]
pub struct GlyphId(pub u32);
@ -69,59 +77,66 @@ impl<F> FontContext<F> where F: Loader {
FontContext { font_info: HashMap::new() }
}
pub fn push_glyph(&mut self,
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> {
let font_key = font.postscript_name();
let metrics = font.metrics();
// Insert the font into the cache if needed.
if let Some(ref font_key) = font_key {
let mut font_info = match font_key {
Some(font_key) => {
if !self.font_info.contains_key(&*font_key) {
self.font_info.insert((*font_key).clone(), FontInfo::new((*font).clone()));
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());
}
}
}
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<DefaultLoader> {
style: &TextStyle,
render_options: &FontRenderOptions)
-> Result<(), GlyphLoadingError> {
let mut cached_font_key: Option<CachedFontKey<DefaultLoader>> = 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<DefaultLoader> {
}
}
struct CachedFontKey<F> where F: Loader {
font: Arc<F>,
key: Option<String>,
}
impl<F> FontInfo<F> where F: Loader {
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,
}
}
}