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
Changes from all commits
726f7d7
f161ab8
7883718
f3cb7a1
43051c7
5493424
15fd256
c21fde3
34d0c31
d1241a8
6c86429
8372cf1
502f34a
File filter...
Jump to…
Some generated files are not rendered by default. Learn more.
| @@ -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
utsavoza
Author
Contributor
|
||
|
|
||
| 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
jdm
Member
|
||
| 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 | ||
| } | ||
I think instead we can store the CanvasFontContext in CanvasPaintThread and pass a reference as an argument to fill_text.