aboutsummaryrefslogtreecommitdiffstats
path: root/components/canvas/canvas_data.rs
diff options
context:
space:
mode:
authorbors-servo <servo-ops@mozilla.com>2020-06-12 13:43:51 -0400
committerGitHub <noreply@github.com>2020-06-12 13:43:51 -0400
commit721271dcd3c20db5ca8cf146e2b5907647afb4d6 (patch)
tree75360f129a6172fd64040d46d88bdc2a8b0f66d0 /components/canvas/canvas_data.rs
parentcb92a15600771a69a796f88975d8100f4be296ae (diff)
parent502f34a9db36202cd89f7a1b48bd138d2ce6f46e (diff)
downloadservo-721271dcd3c20db5ca8cf146e2b5907647afb4d6.tar.gz
servo-721271dcd3c20db5ca8cf146e2b5907647afb4d6.zip
Auto merge of #26697 - utsavoza:ugo/issue-11681/22-05-2020, r=jdm
Implement CanvasRenderingContext2d.fillText The PR consists of broadly two main changes: - Implementation of Canvas2dRenderingContext.font - Basic implementation of Canvas2dRenderingContext.fillText Although I am not fully sure about the long term goals for the canvas backend in Servo, I assumed limited scope for font and text handling (should support simple text drawing with font selection) in the current implementation as I believe a more complete implementation would eventually be brought in as a part of #22957. --- - [x] `./mach build -d` does not report any errors - [x] `./mach test-tidy` does not report any errors - [x] These changes fix #11681 - [x] There are tests for these changes
Diffstat (limited to 'components/canvas/canvas_data.rs')
-rw-r--r--components/canvas/canvas_data.rs234
1 files changed, 226 insertions, 8 deletions
diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs
index 33da592c235..402c2690257 100644
--- a/components/canvas/canvas_data.rs
+++ b/components/canvas/canvas_data.rs
@@ -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())
+ }))
+ })
+}
+
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.
+ 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>) {
@@ -1042,6 +1181,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) &&
@@ -1121,6 +1272,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,
@@ -1133,6 +1287,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
@@ -1214,3 +1371,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
+}