mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
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
This commit is contained in:
commit
721271dcd3
57 changed files with 928 additions and 183 deletions
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue