diff --git a/components/canvas/canvas_data.rs b/components/canvas/canvas_data.rs index a1617be42dcd..ec59074582a3 100644 --- a/components/canvas/canvas_data.rs +++ b/components/canvas/canvas_data.rs @@ -7,9 +7,10 @@ use crate::raqote_backend::Repetition; use canvas_traits::canvas::*; use cssparser::RGBA; use euclid::default::{Point2D, Rect, Size2D, Transform2D, Vector2D}; -use euclid::point2; +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; @@ -281,7 +282,7 @@ pub trait GenericDrawTarget { point_size: f32, text: &str, start: Point2D, - pattern: Pattern, + pattern: &Pattern, draw_options: &DrawOptions, ); fn fill_rect(&mut self, rect: &Rect, pattern: Pattern, draw_options: Option<&DrawOptions>); @@ -499,13 +500,13 @@ impl<'a> CanvasData<'a> { text: String, x: f64, y: f64, - _max_width: Option, - _is_rtl: bool, + max_width: Option, + is_rtl: bool, ) { - // Step 2. Replace all ASCII whitespace in text with U+0020 SPACE characters. + // Step 2. let text = replace_ascii_whitespace(text); - // Step 3. Let font be the current font of target, as given by that object's font attribute. + // Step 3. let point_size = self .state .font_style @@ -513,12 +514,18 @@ impl<'a> CanvasData<'a> { .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(font_style), + || 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(""); + 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))) @@ -528,15 +535,73 @@ impl<'a> CanvasData<'a> { }) }, ); - let start = point2(x as f32, y as f32); + 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: Process bidi text + // 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. - let fill_style = self.state.fill_style.clone(); - let draw_options = &self.state.draw_options; - self.drawtarget - .fill_text(&font, point_size, &text, start, fill_style, draw_options); + 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 { + 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) { @@ -1206,6 +1271,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, @@ -1321,19 +1389,23 @@ fn load_system_font_from_style(font_style: Option<&FontStyleStruct>) -> Font { .stretch(style.font_stretch.into()); let font_handle = match SystemSource::new().select_best_match(&family_names, &properties) { Ok(handle) => handle, - Err(_) => return load_default_system_fallback_font(&properties), + Err(e) => { + error!("error getting font handle for style {:?}: {}", style, e); + return load_default_system_fallback_font(&properties); + }, }; - font_handle - .load() - .unwrap_or_else(|_| 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) - .unwrap() + .expect("error getting font handle for default system font") .load() - .unwrap() + .expect("error loading default system font") } fn replace_ascii_whitespace(text: String) -> String { @@ -1344,3 +1416,18 @@ fn replace_ascii_whitespace(text: String) -> String { }) .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 +} diff --git a/components/canvas/raqote_backend.rs b/components/canvas/raqote_backend.rs index 5bab459b3261..5f39a1260b3d 100644 --- a/components/canvas/raqote_backend.rs +++ b/components/canvas/raqote_backend.rs @@ -76,9 +76,6 @@ impl Backend for RaqoteBackend { } impl<'a> CanvasPaintState<'a> { - pub const HANGING_BASELINE_DEFAULT: f32 = 0.8; // fraction of ascent - pub const IDEOGRAPHIC_BASELINE_DEFAULT: f32 = 0.5; // fraction descent - pub fn new(_antialias: AntialiasMode) -> CanvasPaintState<'a> { let pattern = Pattern::Color(255, 0, 0, 0); CanvasPaintState { @@ -92,8 +89,8 @@ impl<'a> CanvasPaintState<'a> { shadow_blur: 0.0, shadow_color: Color::Raqote(raqote::SolidSource::from_unpremultiplied_argb(0, 0, 0, 0)), font_style: None, - text_align: Default::default(), - text_baseline: Default::default(), + text_align: TextAlign::default(), + text_baseline: TextBaseline::default(), } } } @@ -527,7 +524,7 @@ impl GenericDrawTarget for raqote::DrawTarget { point_size: f32, text: &str, start: Point2D, - pattern: canvas_data::Pattern, + pattern: &canvas_data::Pattern, options: &DrawOptions, ) { self.draw_text( diff --git a/components/canvas_traits/canvas.rs b/components/canvas_traits/canvas.rs index 836285863e85..f302aaa8e3e9 100644 --- a/components/canvas_traits/canvas.rs +++ b/components/canvas_traits/canvas.rs @@ -8,7 +8,7 @@ use ipc_channel::ipc::{IpcBytesReceiver, IpcBytesSender, IpcSender, IpcSharedMem use serde_bytes::ByteBuf; use std::default::Default; use std::str::FromStr; -use style::properties::style_structs::Font; +use style::properties::style_structs::Font as FontStyleStruct; #[derive(Clone, Debug, Deserialize, Serialize)] pub enum FillRule { @@ -71,7 +71,7 @@ pub enum Canvas2dMsg { SetShadowOffsetY(f64), SetShadowBlur(f64), SetShadowColor(RGBA), - SetFont(Font), + SetFont(FontStyleStruct), SetTextAlign(TextAlign), SetTextBaseline(TextBaseline), } diff --git a/components/layout_2020/query.rs b/components/layout_2020/query.rs index a8a27a30faaf..6df2e789e645 100644 --- a/components/layout_2020/query.rs +++ b/components/layout_2020/query.rs @@ -387,3 +387,11 @@ pub fn process_element_inner_text_query<'dom>(_node: impl LayoutNode<'dom>) -> S pub fn process_text_index_request(_node: OpaqueNode, _point: Point2D) -> TextIndexResponse { TextIndexResponse(None) } + +pub fn process_resolved_font_style_query<'dom>( + _node: impl LayoutNode<'dom>, + _property: &PropertyId, + _value: &str, +) -> Option> { + None +} diff --git a/components/layout_thread_2020/lib.rs b/components/layout_thread_2020/lib.rs index bdfb75a23234..4ae69e6255ad 100644 --- a/components/layout_thread_2020/lib.rs +++ b/components/layout_thread_2020/lib.rs @@ -37,7 +37,8 @@ use layout::context::LayoutContext; use layout::display_list::{DisplayListBuilder, WebRenderImageInfo}; use layout::layout_debug; use layout::query::{ - process_content_box_request, process_content_boxes_request, LayoutRPCImpl, LayoutThreadData, + process_content_box_request, process_content_boxes_request, process_resolved_font_style_query, + LayoutRPCImpl, LayoutThreadData, }; use layout::query::{process_element_inner_text_query, process_node_geometry_request}; use layout::query::{process_node_scroll_area_request, process_node_scroll_id_request}; @@ -525,6 +526,7 @@ impl LayoutThread { scroll_id_response: None, scroll_area_response: Rect::zero(), resolved_style_response: String::new(), + resolved_font_style_response: None, offset_parent_response: OffsetParentResponse::empty(), scroll_offsets: HashMap::new(), text_index_response: TextIndexResponse(None), @@ -1209,7 +1211,11 @@ impl LayoutThread { fragment_tree, ); }, - &QueryMsg::ResolvedFontStyleQuery(_, _, _) => unimplemented!(), + &QueryMsg::ResolvedFontStyleQuery(node, ref property, ref value) => { + let node = unsafe { ServoLayoutNode::new(&node) }; + rw_data.resolved_font_style_response = + process_resolved_font_style_query(node, property, value); + }, &QueryMsg::OffsetParentQuery(node) => { rw_data.offset_parent_response = process_offset_parent_query(node); }, diff --git a/components/script/canvas_state.rs b/components/script/canvas_state.rs index fa6728c34e1d..0a6fba842e46 100644 --- a/components/script/canvas_state.rs +++ b/components/script/canvas_state.rs @@ -1021,7 +1021,13 @@ impl CanvasState { if self.state.borrow().font_style.is_none() { self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into()) } - let is_rtl = false; // TODO: resolve is_rtl wrt to canvas element + + let is_rtl = match self.state.borrow().direction { + Direction::Ltr => false, + Direction::Rtl => true, + Direction::Inherit => false, // TODO: resolve direction wrt to canvas element + }; + let style = self.state.borrow().fill_style.to_fill_or_stroke_style(); self.send_canvas_2d_msg(Canvas2dMsg::FillText( text.into(), @@ -1046,7 +1052,7 @@ impl CanvasState { pub fn set_font(&self, canvas: Option<&HTMLCanvasElement>, value: DOMString) { let canvas = match canvas { Some(element) => element, - None => return, + None => return, // offscreen canvas doesn't have a placeholder canvas }; let node = canvas.upcast::(); let window = window_from_node(&*canvas);