Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement CanvasRenderingContext2d.fillText #26697

Merged
merged 13 commits into from Jun 12, 2020
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file
Failed to load files.

Always

Just for now

Some generated files are not rendered by default. Learn more.

@@ -22,17 +22,21 @@ canvas_traits = { path = "../canvas_traits" }
crossbeam-channel = "0.4"
cssparser = "0.27"
euclid = "0.20"
font-kit = "0.7"
fnv = "1.0"
gfx = { path = "../gfx" }
gleam = "0.11"
half = "1"
ipc-channel = "0.14"
log = "0.4"
lyon_geom = "0.14"
num-traits = "0.2"
pixels = { path = "../pixels" }
raqote = "0.8"
raqote = { version = "0.8", features = ["text"] }
servo_arc = { path = "../servo_arc" }
servo_config = { path = "../config" }
sparkle = "0.1.24"
style = { path = "../style" }
# NOTE: the sm-angle feature only enables ANGLE on Windows, not other platforms!
surfman = { version = "0.2", features = ["sm-angle", "sm-angle-default"] }
surfman-chains = "0.3"
@@ -7,12 +7,24 @@ use crate::raqote_backend::Repetition;
use canvas_traits::canvas::*;
use cssparser::RGBA;
use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D};
use euclid::{point2, vec2};
use font_kit::family_name::FamilyName;
use font_kit::font::Font;
use font_kit::metrics::Metrics;
use font_kit::properties::Properties;
use font_kit::source::SystemSource;
use gfx::font::FontHandleMethods;
use gfx::font_cache_thread::FontCacheThread;
use gfx::font_context::FontContext;
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
use num_traits::ToPrimitive;
use servo_arc::Arc as ServoArc;
use std::cell::RefCell;
#[allow(unused_imports)]
use std::marker::PhantomData;
use std::mem;
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use style::properties::style_structs::Font as FontStyleStruct;
use webrender_api::units::RectExt as RectExt_;

/// The canvas data stores a state machine for the current status of
@@ -264,6 +276,15 @@ pub trait GenericDrawTarget {
operator: CompositionOp,
);
fn fill(&mut self, path: &Path, pattern: Pattern, draw_options: &DrawOptions);
fn fill_text(
&mut self,
font: &Font,
point_size: f32,
text: &str,
start: Point2D<f32>,
pattern: &Pattern,
draw_options: &DrawOptions,
);
fn fill_rect(&mut self, rect: &Rect<f32>, pattern: Pattern, draw_options: Option<&DrawOptions>);
fn get_format(&self) -> SurfaceFormat;
fn get_size(&self) -> Size2D<i32>;
@@ -360,6 +381,21 @@ pub enum Filter {
Point,
}

pub(crate) type CanvasFontContext = FontContext<FontCacheThread>;

thread_local!(static FONT_CONTEXT: RefCell<Option<CanvasFontContext>> = RefCell::new(None));

pub(crate) fn with_thread_local_font_context<F, R>(canvas_data: &CanvasData, f: F) -> R
where
F: FnOnce(&mut CanvasFontContext) -> R,
{
FONT_CONTEXT.with(|font_context| {
f(font_context.borrow_mut().get_or_insert_with(|| {
FontContext::new(canvas_data.font_cache_thread.lock().unwrap().clone())
}))
})
}
Comment on lines +384 to +397

This comment has been minimized.

Copy link
@jdm

jdm Jun 11, 2020

Member

I think instead we can store the CanvasFontContext in CanvasPaintThread and pass a reference as an argument to fill_text.

This comment has been minimized.

Copy link
@utsavoza

utsavoza Jun 12, 2020

Author Contributor

Actually, we require a mutable reference to CanvasFontContext and storing it CanvasPaintThread and passing a mutable reference in fillText might make the borrow checker unhappy.

This comment has been minimized.

Copy link
@jdm

jdm Jun 12, 2020

Member

I'm not sure I understand why that would be a problem?

This comment has been minimized.

Copy link
@utsavoza

utsavoza Jun 12, 2020

Author Contributor

I think storing CanvasFontContext and getting a mutable reference will lead to multiple mutable borrows on self here:

Canvas2dMsg::FillText(text, x, y, max_width, style, is_rtl) => {
    self.canvas(canvas_id).set_fill_style(style);
    self.canvas(canvas_id)
        .fill_text(text, x, y, max_width, is_rtl);
},

as we are calling fill_text on mutably borrowed CanvasData from CanvasPaintThread.


pub struct CanvasData<'a> {
backend: Box<dyn Backend>,
drawtarget: Box<dyn GenericDrawTarget>,
@@ -372,7 +408,7 @@ pub struct CanvasData<'a> {
old_image_key: Option<webrender_api::ImageKey>,
/// An old webrender image key that can be deleted when the current epoch ends.
very_old_image_key: Option<webrender_api::ImageKey>,
pub canvas_id: CanvasId,
font_cache_thread: Mutex<FontCacheThread>,
}

fn create_backend() -> Box<dyn Backend> {
@@ -384,7 +420,7 @@ impl<'a> CanvasData<'a> {
size: Size2D<u64>,
webrender_api: Box<dyn WebrenderApi>,
antialias: AntialiasMode,
canvas_id: CanvasId,
font_cache_thread: FontCacheThread,
) -> CanvasData<'a> {
let backend = create_backend();
let draw_target = backend.create_drawtarget(size);
@@ -398,7 +434,7 @@ impl<'a> CanvasData<'a> {
image_key: None,
old_image_key: None,
very_old_image_key: None,
canvas_id: canvas_id,
font_cache_thread: Mutex::new(font_cache_thread),
}
}

@@ -456,11 +492,114 @@ impl<'a> CanvasData<'a> {
}
}

pub fn fill_text(&self, text: String, x: f64, y: f64, max_width: Option<f64>) {
error!(
"Unimplemented canvas2d.fillText. Values received: {}, {}, {}, {:?}.",
text, x, y, max_width
// https://html.spec.whatwg.org/multipage/#text-preparation-algorithm
pub fn fill_text(
&mut self,
text: String,
x: f64,
y: f64,
max_width: Option<f64>,
is_rtl: bool,
) {
// Step 2.
This conversation was marked as resolved by utsavoza

This comment has been minimized.

Copy link
@jdm

jdm Jun 11, 2020

Member

Let's add a comment somewhere linking to the spec these steps refer to. It looks like https://html.spec.whatwg.org/multipage/canvas.html#text-preparation-algorithm to me?

let text = replace_ascii_whitespace(text);

// Step 3.
let point_size = self
.state
.font_style
.as_ref()
.map_or(10., |style| style.font_size.size().px());
let font_style = self.state.font_style.as_ref();
let font = font_style.map_or_else(
|| load_system_font_from_style(None),
|style| {
with_thread_local_font_context(&self, |font_context| {
let font_group = font_context.font_group(ServoArc::new(style.clone()));
let font = font_group
.borrow_mut()
.first(font_context)
.expect("couldn't find font");
let font = font.borrow_mut();
// Retrieving bytes from font template seems to panic for some core text fonts.
// This check avoids having to obtain bytes from the font template data if they
// are not already in the memory.
if let Some(bytes) = font.handle.template().bytes_if_in_memory() {
Font::from_bytes(Arc::new(bytes), 0)
.unwrap_or_else(|_| load_system_font_from_style(Some(style)))
} else {
load_system_font_from_style(Some(style))
}
})
},
);
let font_width = font_width(&text, point_size, &font);

// Step 6.
let max_width = max_width.map(|width| width as f32);
let (width, scale_factor) = match max_width {
Some(max_width) if max_width > font_width => (max_width, 1.),
Some(max_width) => (font_width, max_width / font_width),
None => (font_width, 1.),
};

// Step 7.
let start = self.text_origin(x as f32, y as f32, &font.metrics(), width, is_rtl);

// TODO: Bidi text layout

let old_transform = self.get_transform();
self.set_transform(
&old_transform
.pre_translate(vec2(start.x, 0.))
.pre_scale(scale_factor, 1.)
.pre_translate(vec2(-start.x, 0.)),
);

// Step 8.
self.drawtarget.fill_text(
&font,
point_size,
&text,
start,
&self.state.fill_style,
&self.state.draw_options,
);

self.set_transform(&old_transform);
}

fn text_origin(
&self,
x: f32,
y: f32,
metrics: &Metrics,
width: f32,
is_rtl: bool,
) -> Point2D<f32> {
let text_align = match self.state.text_align {
TextAlign::Start if is_rtl => TextAlign::Right,
TextAlign::Start => TextAlign::Left,
TextAlign::End if is_rtl => TextAlign::Left,
TextAlign::End => TextAlign::Right,
text_align => text_align,
};
let anchor_x = match text_align {
TextAlign::Center => -width / 2.,
TextAlign::Right => -width,
_ => 0.,
};

let anchor_y = match self.state.text_baseline {
TextBaseline::Top => metrics.ascent,
TextBaseline::Hanging => metrics.ascent * HANGING_BASELINE_DEFAULT,
TextBaseline::Ideographic => -metrics.descent * IDEOGRAPHIC_BASELINE_DEFAULT,
TextBaseline::Middle => (metrics.ascent - metrics.descent) / 2.,
TextBaseline::Alphabetic => 0.,
TextBaseline::Bottom => -metrics.descent,
};

point2(x + anchor_x, y + anchor_y)
}

pub fn fill_rect(&mut self, rect: &Rect<f32>) {
@@ -1039,6 +1178,18 @@ impl<'a> CanvasData<'a> {
self.backend.set_shadow_color(value, &mut self.state);
}

pub fn set_font(&mut self, font_style: FontStyleStruct) {
self.state.font_style = Some(font_style)
}

pub fn set_text_align(&mut self, text_align: TextAlign) {
self.state.text_align = text_align;
}

pub fn set_text_baseline(&mut self, text_baseline: TextBaseline) {
self.state.text_baseline = text_baseline;
}

// https://html.spec.whatwg.org/multipage/#when-shadows-are-drawn
fn need_to_draw_shadow(&self) -> bool {
self.backend.need_to_draw_shadow(&self.state.shadow_color) &&
@@ -1118,6 +1269,9 @@ impl<'a> Drop for CanvasData<'a> {
}
}

const HANGING_BASELINE_DEFAULT: f32 = 0.8;
const IDEOGRAPHIC_BASELINE_DEFAULT: f32 = 0.5;

#[derive(Clone)]
pub struct CanvasPaintState<'a> {
pub draw_options: DrawOptions,
@@ -1130,6 +1284,9 @@ pub struct CanvasPaintState<'a> {
pub shadow_offset_y: f64,
pub shadow_blur: f64,
pub shadow_color: Color,
pub font_style: Option<FontStyleStruct>,
pub text_align: TextAlign,
pub text_baseline: TextBaseline,
}

/// It writes an image to the destination target
@@ -1211,3 +1368,64 @@ impl RectExt for Rect<u32> {
self.cast()
}
}

fn load_system_font_from_style(font_style: Option<&FontStyleStruct>) -> Font {
let mut properties = Properties::new();
let style = match font_style {
Some(style) => style,
None => return load_default_system_fallback_font(&properties),
};
let family_names = style
.font_family
.families
.iter()
.map(|family_name| family_name.into())
.collect::<Vec<FamilyName>>();
let properties = properties
.style(style.font_style.into())
.weight(style.font_weight.into())
.stretch(style.font_stretch.into());
let font_handle = match SystemSource::new().select_best_match(&family_names, &properties) {
Ok(handle) => handle,
Err(e) => {
error!("error getting font handle for style {:?}: {}", style, e);
return load_default_system_fallback_font(&properties);
},
};
font_handle.load().unwrap_or_else(|e| {
error!("error loading font for style {:?}: {}", style, e);
load_default_system_fallback_font(&properties)
})
}

fn load_default_system_fallback_font(properties: &Properties) -> Font {
SystemSource::new()
.select_best_match(&[FamilyName::SansSerif], properties)
.expect("error getting font handle for default system font")
.load()
.expect("error loading default system font")
}

fn replace_ascii_whitespace(text: String) -> String {
text.chars()
.map(|c| match c {
' ' | '\t' | '\n' | '\r' | '\x0C' => '\x20',
_ => c,
})
.collect()
}

// TODO: This currently calculates the width using just advances and doesn't
// determine the fallback font in case a character glyph isn't found.
fn font_width(text: &str, point_size: f32, font: &Font) -> f32 {
let metrics = font.metrics();
let mut width = 0.;
for c in text.chars() {
if let Some(glyph_id) = font.glyph_for_char(c) {
if let Ok(advance) = font.advance(glyph_id) {
width += advance.x() * point_size / metrics.units_per_em as f32;
}
}
}
width
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.