diff options
author | bors-servo <servo-ops@mozilla.com> | 2020-06-12 13:43:51 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-12 13:43:51 -0400 |
commit | 721271dcd3c20db5ca8cf146e2b5907647afb4d6 (patch) | |
tree | 75360f129a6172fd64040d46d88bdc2a8b0f66d0 /components/canvas/canvas_data.rs | |
parent | cb92a15600771a69a796f88975d8100f4be296ae (diff) | |
parent | 502f34a9db36202cd89f7a1b48bd138d2ce6f46e (diff) | |
download | servo-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.rs | 234 |
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 +} |