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:
commit
ca201ac0c8
|
@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
||||||
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,
|
mut position: Vector2F,
|
||||||
paint_id: PaintId,
|
paint_id: PaintId,
|
||||||
render_mode: TextRenderMode) {
|
render_mode: TextRenderMode)
|
||||||
let layout = self.layout_text(string);
|
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();
|
||||||
|
|
||||||
position += layout.metrics().text_origin(&self.current_state);
|
position += layout.text_origin();
|
||||||
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,
|
||||||
|
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::Left => 0.0,
|
||||||
TextAlign::Right => -self.width,
|
TextAlign::Right => -self.width(),
|
||||||
TextAlign::Center => -0.5 * 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::Alphabetic => 0.0,
|
||||||
TextBaseline::Top => self.em_height_ascent,
|
TextBaseline::Top => vertical_metrics.em_height_ascent,
|
||||||
TextBaseline::Middle => util::lerp(self.em_height_ascent, self.em_height_descent, 0.5),
|
TextBaseline::Middle => {
|
||||||
TextBaseline::Bottom => self.em_height_descent,
|
util::lerp(vertical_metrics.em_height_ascent,
|
||||||
TextBaseline::Ideographic => self.ideographic_baseline,
|
vertical_metrics.em_height_descent,
|
||||||
TextBaseline::Hanging => self.hanging_baseline,
|
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;
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
font_key: Option<&str>,
|
||||||
glyph_id: GlyphId,
|
glyph_id: GlyphId,
|
||||||
|
glyph_offset: Vector2F,
|
||||||
|
font_size: f32,
|
||||||
render_options: &FontRenderOptions)
|
render_options: &FontRenderOptions)
|
||||||
-> Result<(), GlyphLoadingError> {
|
-> Result<(), GlyphLoadingError> {
|
||||||
let font_key = font.postscript_name();
|
|
||||||
let metrics = font.metrics();
|
|
||||||
|
|
||||||
// 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 {
|
||||||
|
Some(font_key) => {
|
||||||
if !self.font_info.contains_key(&*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.
|
// 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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue