mirror of
https://github.com/servo/servo.git
synced 2025-09-04 03:58:23 +01:00
canvas: Move font selection and text shaping to script
(#38979)
Instead of doing font selection and text shaping in `canvas`, move this to `script`. This allows canvas to use the shared `Document` `FontContext`, which has access to web fonts. In addition, ensure that there is a font style accessible for `OffscreenCanvas` in workers. Testing: This causes a number of WPT tests to start to pass as web fonts are supported on canvas again. In addition, some start to fail as they expose other issues: - The lack of support for the `Context2D.fontStretch` property - Issues with zerosize gradient interpolation. - Differences between quoted and unquoted font family names. This seems like a timing issue with the way we are handling web fonts. The test seems to be expecting Local fonts to be available immediately (without waiting for them to load). This isn't how Servo works ATM. Seems like an issue with the test. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
91b27d98a2
commit
cb64def7e6
49 changed files with 604 additions and 809 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1171,6 +1171,7 @@ dependencies = [
|
|||
"base",
|
||||
"crossbeam-channel",
|
||||
"euclid",
|
||||
"fonts_traits",
|
||||
"glow",
|
||||
"ipc-channel",
|
||||
"kurbo",
|
||||
|
@ -7407,6 +7408,7 @@ dependencies = [
|
|||
"timers",
|
||||
"tracing",
|
||||
"unicode-bidi",
|
||||
"unicode-script",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"urlpattern",
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use canvas_traits::canvas::{
|
||||
CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions,
|
||||
CompositionOptions, FillOrStrokeStyle, FillRule, LineOptions, Path, ShadowOptions, TextRun,
|
||||
};
|
||||
use compositing_traits::SerializableImageData;
|
||||
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
|
||||
use pixels::Snapshot;
|
||||
use webrender_api::ImageDescriptor;
|
||||
|
||||
use crate::canvas_data::{Filter, TextRun};
|
||||
use crate::canvas_data::Filter;
|
||||
|
||||
// This defines required methods for a DrawTarget. The prototypes are derived from the now-removed
|
||||
// Azure backend's methods.
|
||||
|
@ -55,7 +55,6 @@ pub(crate) trait GenericDrawTarget {
|
|||
fn fill_text(
|
||||
&mut self,
|
||||
text_runs: Vec<TextRun>,
|
||||
start: Point2D<f32>,
|
||||
style: FillOrStrokeStyle,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
|
|
|
@ -2,22 +2,10 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
|
||||
use app_units::Au;
|
||||
use canvas_traits::canvas::*;
|
||||
use compositing_traits::CrossProcessCompositorApi;
|
||||
use euclid::default::{Box2D, Point2D, Rect, Size2D, Transform2D, Vector2D};
|
||||
use euclid::point2;
|
||||
use fonts::{
|
||||
ByteIndex, FontBaseline, FontContext, FontGroup, FontMetrics, FontRef, GlyphInfo, GlyphStore,
|
||||
LAST_RESORT_GLYPH_ADVANCE, ShapingFlags, ShapingOptions,
|
||||
};
|
||||
use log::warn;
|
||||
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
|
||||
use pixels::Snapshot;
|
||||
use range::Range;
|
||||
use unicode_script::Script;
|
||||
use webrender_api::ImageKey;
|
||||
|
||||
use crate::backend::GenericDrawTarget;
|
||||
|
@ -26,76 +14,6 @@ use crate::backend::GenericDrawTarget;
|
|||
// https://github.com/servo/webrender/blob/main/webrender/src/texture_cache.rs#L1475
|
||||
const MIN_WR_IMAGE_SIZE: Size2D<u64> = Size2D::new(1, 1);
|
||||
|
||||
#[derive(Default)]
|
||||
struct UnshapedTextRun<'a> {
|
||||
font: Option<FontRef>,
|
||||
script: Script,
|
||||
string: &'a str,
|
||||
}
|
||||
|
||||
impl UnshapedTextRun<'_> {
|
||||
fn script_and_font_compatible(&self, script: Script, other_font: &Option<FontRef>) -> bool {
|
||||
if self.script != script {
|
||||
return false;
|
||||
}
|
||||
|
||||
match (&self.font, other_font) {
|
||||
(Some(font_a), Some(font_b)) => font_a.identifier() == font_b.identifier(),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_shaped_text_run(self) -> Option<TextRun> {
|
||||
let font = self.font?;
|
||||
if self.string.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let word_spacing = Au::from_f64_px(
|
||||
font.glyph_index(' ')
|
||||
.map(|glyph_id| font.glyph_h_advance(glyph_id))
|
||||
.unwrap_or(LAST_RESORT_GLYPH_ADVANCE),
|
||||
);
|
||||
let options = ShapingOptions {
|
||||
letter_spacing: None,
|
||||
word_spacing,
|
||||
script: self.script,
|
||||
flags: ShapingFlags::empty(),
|
||||
};
|
||||
let glyphs = font.shape_text(self.string, &options);
|
||||
Some(TextRun { font, glyphs })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TextRun {
|
||||
pub(crate) font: FontRef,
|
||||
pub(crate) glyphs: Arc<GlyphStore>,
|
||||
}
|
||||
|
||||
impl TextRun {
|
||||
fn bounding_box(&self) -> Rect<f32> {
|
||||
let mut bounding_box = None;
|
||||
let mut bounds_offset: f32 = 0.;
|
||||
let glyph_ids = self
|
||||
.glyphs
|
||||
.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), self.glyphs.len()))
|
||||
.map(GlyphInfo::id);
|
||||
for glyph_id in glyph_ids {
|
||||
let bounds = self.font.typographic_bounds(glyph_id);
|
||||
let amount = Vector2D::new(bounds_offset, 0.);
|
||||
let bounds = bounds.translate(amount);
|
||||
let initiated_bbox = bounding_box.get_or_insert_with(|| {
|
||||
let origin = Point2D::new(bounds.min_x(), 0.);
|
||||
Box2D::new(origin, origin).to_rect()
|
||||
});
|
||||
bounding_box = Some(initiated_bbox.union(&bounds));
|
||||
bounds_offset = bounds.max_x();
|
||||
}
|
||||
bounding_box.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum Filter {
|
||||
Bilinear,
|
||||
|
@ -106,14 +24,12 @@ pub(crate) struct CanvasData<DrawTarget: GenericDrawTarget> {
|
|||
drawtarget: DrawTarget,
|
||||
compositor_api: CrossProcessCompositorApi,
|
||||
image_key: ImageKey,
|
||||
font_context: Arc<FontContext>,
|
||||
}
|
||||
|
||||
impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
|
||||
pub(crate) fn new(
|
||||
size: Size2D<u64>,
|
||||
compositor_api: CrossProcessCompositorApi,
|
||||
font_context: Arc<FontContext>,
|
||||
) -> CanvasData<DrawTarget> {
|
||||
let size = size.max(MIN_WR_IMAGE_SIZE);
|
||||
let mut draw_target = DrawTarget::new(size.cast());
|
||||
|
@ -124,7 +40,6 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
|
|||
drawtarget: draw_target,
|
||||
compositor_api,
|
||||
image_key,
|
||||
font_context,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,296 +96,26 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn fill_text_with_size(
|
||||
&mut self,
|
||||
text: String,
|
||||
x: f64,
|
||||
y: f64,
|
||||
max_width: Option<f64>,
|
||||
is_rtl: bool,
|
||||
size: f64,
|
||||
style: FillOrStrokeStyle,
|
||||
text_options: &TextOptions,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
) {
|
||||
// > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
|
||||
let text = replace_ascii_whitespace(text);
|
||||
|
||||
// > Step 3: Let font be the current font of target, as given by that object's font
|
||||
// > attribute.
|
||||
let Some(ref font_style) = text_options.font else {
|
||||
return;
|
||||
};
|
||||
|
||||
let font_group = self
|
||||
.font_context
|
||||
.font_group_with_size(font_style.clone(), Au::from_f64_px(size));
|
||||
let mut font_group = font_group.write();
|
||||
let Some(first_font) = font_group.first(&self.font_context) else {
|
||||
warn!("Could not render canvas text, because there was no first font.");
|
||||
return;
|
||||
};
|
||||
|
||||
let runs = self.build_unshaped_text_runs(&text, &mut font_group);
|
||||
// TODO: This doesn't do any kind of line layout at all. In particular, there needs
|
||||
// to be some alignment along a baseline and also support for bidi text.
|
||||
let shaped_runs: Vec<_> = runs
|
||||
.into_iter()
|
||||
.filter_map(UnshapedTextRun::into_shaped_text_run)
|
||||
.collect();
|
||||
let total_advance = shaped_runs
|
||||
.iter()
|
||||
.map(|run| run.glyphs.total_advance())
|
||||
.sum::<Au>()
|
||||
.to_f64_px();
|
||||
|
||||
// > Step 6: If maxWidth was provided and the hypothetical width of the inline box in the
|
||||
// > hypothetical line box is greater than maxWidth CSS pixels, then change font to have a
|
||||
// > more condensed font (if one is available or if a reasonably readable one can be
|
||||
// > synthesized by applying a horizontal scale factor to the font) or a smaller font, and
|
||||
// > return to the previous step.
|
||||
//
|
||||
// TODO: We only try decreasing the font size here. Eventually it would make sense to use
|
||||
// other methods to try to decrease the size, such as finding a narrower font or decreasing
|
||||
// spacing.
|
||||
if let Some(max_width) = max_width {
|
||||
let new_size = (max_width / total_advance * size).floor().max(5.);
|
||||
if total_advance > max_width && new_size != size {
|
||||
self.fill_text_with_size(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
Some(max_width),
|
||||
is_rtl,
|
||||
new_size,
|
||||
style,
|
||||
text_options,
|
||||
composition_options,
|
||||
transform,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// > Step 7: Find the anchor point for the line of text.
|
||||
let start = self.find_anchor_point_for_line_of_text(
|
||||
x as f32,
|
||||
y as f32,
|
||||
&first_font.metrics,
|
||||
total_advance as f32,
|
||||
is_rtl,
|
||||
text_options,
|
||||
);
|
||||
|
||||
// > Step 8: Let result be an array constructed by iterating over each glyph in the inline box
|
||||
// > from left to right (if any), adding to the array, for each glyph, the shape of the glyph
|
||||
// > as it is in the inline box, positioned on a coordinate space using CSS pixels with its
|
||||
// > origin is at the anchor point.
|
||||
self.maybe_bound_shape_with_pattern(
|
||||
style,
|
||||
composition_options,
|
||||
&Rect::from_size(Size2D::new(total_advance, size)),
|
||||
transform,
|
||||
|self_, style| {
|
||||
self_.drawtarget.fill_text(
|
||||
shaped_runs,
|
||||
start,
|
||||
style,
|
||||
composition_options,
|
||||
transform,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>
|
||||
pub(crate) fn fill_text(
|
||||
&mut self,
|
||||
text: String,
|
||||
x: f64,
|
||||
y: f64,
|
||||
max_width: Option<f64>,
|
||||
is_rtl: bool,
|
||||
style: FillOrStrokeStyle,
|
||||
text_options: TextOptions,
|
||||
text_bounds: Rect<f64>,
|
||||
text_runs: Vec<TextRun>,
|
||||
fill_or_stroke_style: FillOrStrokeStyle,
|
||||
_shadow_options: ShadowOptions,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
) {
|
||||
let Some(ref font_style) = text_options.font else {
|
||||
return;
|
||||
};
|
||||
|
||||
let size = font_style.font_size.computed_size();
|
||||
self.fill_text_with_size(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
max_width,
|
||||
is_rtl,
|
||||
size.px() as f64,
|
||||
style,
|
||||
&text_options,
|
||||
self.maybe_bound_shape_with_pattern(
|
||||
fill_or_stroke_style,
|
||||
composition_options,
|
||||
&text_bounds,
|
||||
transform,
|
||||
);
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-measuretext>
|
||||
pub(crate) fn measure_text(&mut self, text: String, text_options: TextOptions) -> TextMetrics {
|
||||
// > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
|
||||
let text = replace_ascii_whitespace(text);
|
||||
let Some(ref font_style) = text_options.font else {
|
||||
return TextMetrics::default();
|
||||
};
|
||||
|
||||
let font_group = self.font_context.font_group(font_style.clone());
|
||||
let mut font_group = font_group.write();
|
||||
let font = font_group
|
||||
.first(&self.font_context)
|
||||
.expect("couldn't find font");
|
||||
let ascent = font.metrics.ascent.to_f32_px();
|
||||
let descent = font.metrics.descent.to_f32_px();
|
||||
let runs = self.build_unshaped_text_runs(&text, &mut font_group);
|
||||
|
||||
let shaped_runs: Vec<_> = runs
|
||||
.into_iter()
|
||||
.filter_map(UnshapedTextRun::into_shaped_text_run)
|
||||
.collect();
|
||||
let total_advance = shaped_runs
|
||||
.iter()
|
||||
.map(|run| run.glyphs.total_advance())
|
||||
.sum::<Au>()
|
||||
.to_f32_px();
|
||||
let bounding_box = shaped_runs
|
||||
.iter()
|
||||
.map(TextRun::bounding_box)
|
||||
.reduce(|a, b| {
|
||||
let amount = Vector2D::new(a.max_x(), 0.);
|
||||
let bounding_box = b.translate(amount);
|
||||
a.union(&bounding_box)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let FontBaseline {
|
||||
ideographic_baseline,
|
||||
alphabetic_baseline,
|
||||
hanging_baseline,
|
||||
} = match font.baseline() {
|
||||
Some(baseline) => baseline,
|
||||
None => FontBaseline {
|
||||
hanging_baseline: ascent * HANGING_BASELINE_DEFAULT,
|
||||
ideographic_baseline: -descent * IDEOGRAPHIC_BASELINE_DEFAULT,
|
||||
alphabetic_baseline: 0.,
|
||||
|self_, style| {
|
||||
self_
|
||||
.drawtarget
|
||||
.fill_text(text_runs, style, composition_options, transform);
|
||||
},
|
||||
};
|
||||
|
||||
let anchor_x = match text_options.align {
|
||||
TextAlign::End => total_advance,
|
||||
TextAlign::Center => total_advance / 2.,
|
||||
TextAlign::Right => total_advance,
|
||||
_ => 0.,
|
||||
};
|
||||
let anchor_y = match text_options.baseline {
|
||||
TextBaseline::Top => ascent,
|
||||
TextBaseline::Hanging => hanging_baseline,
|
||||
TextBaseline::Ideographic => ideographic_baseline,
|
||||
TextBaseline::Middle => (ascent - descent) / 2.,
|
||||
TextBaseline::Alphabetic => alphabetic_baseline,
|
||||
TextBaseline::Bottom => -descent,
|
||||
};
|
||||
|
||||
TextMetrics {
|
||||
width: total_advance,
|
||||
actual_boundingbox_left: anchor_x - bounding_box.min_x(),
|
||||
actual_boundingbox_right: bounding_box.max_x() - anchor_x,
|
||||
actual_boundingbox_ascent: bounding_box.max_y() - anchor_y,
|
||||
actual_boundingbox_descent: anchor_y - bounding_box.min_y(),
|
||||
font_boundingbox_ascent: ascent - anchor_y,
|
||||
font_boundingbox_descent: descent + anchor_y,
|
||||
em_height_ascent: ascent - anchor_y,
|
||||
em_height_descent: descent + anchor_y,
|
||||
hanging_baseline: hanging_baseline - anchor_y,
|
||||
alphabetic_baseline: alphabetic_baseline - anchor_y,
|
||||
ideographic_baseline: ideographic_baseline - anchor_y,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_unshaped_text_runs<'b>(
|
||||
&self,
|
||||
text: &'b str,
|
||||
font_group: &mut FontGroup,
|
||||
) -> Vec<UnshapedTextRun<'b>> {
|
||||
let mut runs = Vec::new();
|
||||
let mut current_text_run = UnshapedTextRun::default();
|
||||
let mut current_text_run_start_index = 0;
|
||||
|
||||
for (index, character) in text.char_indices() {
|
||||
// TODO: This should ultimately handle emoji variation selectors.
|
||||
let script = Script::from(character);
|
||||
let font = font_group.find_by_codepoint(&self.font_context, character, None, None);
|
||||
|
||||
if !current_text_run.script_and_font_compatible(script, &font) {
|
||||
let previous_text_run = mem::replace(
|
||||
&mut current_text_run,
|
||||
UnshapedTextRun {
|
||||
font: font.clone(),
|
||||
script,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
current_text_run_start_index = index;
|
||||
runs.push(previous_text_run)
|
||||
}
|
||||
|
||||
current_text_run.string =
|
||||
&text[current_text_run_start_index..index + character.len_utf8()];
|
||||
}
|
||||
|
||||
runs.push(current_text_run);
|
||||
runs
|
||||
}
|
||||
|
||||
/// Find the *anchor_point* for the given parameters of a line of text.
|
||||
/// See <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>.
|
||||
fn find_anchor_point_for_line_of_text(
|
||||
&self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
metrics: &FontMetrics,
|
||||
width: f32,
|
||||
is_rtl: bool,
|
||||
text_options: &TextOptions,
|
||||
) -> Point2D<f32> {
|
||||
let text_align = match text_options.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 ascent = metrics.ascent.to_f32_px();
|
||||
let descent = metrics.descent.to_f32_px();
|
||||
let anchor_y = match text_options.baseline {
|
||||
TextBaseline::Top => ascent,
|
||||
TextBaseline::Hanging => ascent * HANGING_BASELINE_DEFAULT,
|
||||
TextBaseline::Ideographic => -descent * IDEOGRAPHIC_BASELINE_DEFAULT,
|
||||
TextBaseline::Middle => (ascent - descent) / 2.,
|
||||
TextBaseline::Alphabetic => 0.,
|
||||
TextBaseline::Bottom => -descent,
|
||||
};
|
||||
|
||||
point2(x + anchor_x, y + anchor_y)
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn fill_rect(
|
||||
|
@ -770,9 +415,6 @@ impl<D: GenericDrawTarget> Drop for CanvasData<D> {
|
|||
}
|
||||
}
|
||||
|
||||
const HANGING_BASELINE_DEFAULT: f32 = 0.8;
|
||||
const IDEOGRAPHIC_BASELINE_DEFAULT: f32 = 0.5;
|
||||
|
||||
/// It writes an image to the destination target
|
||||
/// draw_target: the destination target where the image_data will be copied
|
||||
/// image_data: Pixel information of the image to be written. It takes RGBA8
|
||||
|
@ -830,12 +472,3 @@ impl RectToi32 for Rect<f64> {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_ascii_whitespace(text: String) -> String {
|
||||
text.chars()
|
||||
.map(|c| match c {
|
||||
' ' | '\t' | '\n' | '\r' | '\x0C' => '\x20',
|
||||
_ => c,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
use std::borrow::ToOwned;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::{f32, thread};
|
||||
|
||||
use canvas_traits::ConstellationCanvasMsg;
|
||||
|
@ -12,11 +11,9 @@ use canvas_traits::canvas::*;
|
|||
use compositing_traits::CrossProcessCompositorApi;
|
||||
use crossbeam_channel::{Sender, select, unbounded};
|
||||
use euclid::default::{Rect, Size2D, Transform2D};
|
||||
use fonts::{FontContext, SystemFontServiceProxy};
|
||||
use ipc_channel::ipc::{self, IpcSender};
|
||||
use ipc_channel::router::ROUTER;
|
||||
use log::warn;
|
||||
use net_traits::ResourceThreads;
|
||||
use pixels::Snapshot;
|
||||
use webrender_api::ImageKey;
|
||||
|
||||
|
@ -26,24 +23,14 @@ pub struct CanvasPaintThread {
|
|||
canvases: HashMap<CanvasId, Canvas>,
|
||||
next_canvas_id: CanvasId,
|
||||
compositor_api: CrossProcessCompositorApi,
|
||||
font_context: Arc<FontContext>,
|
||||
}
|
||||
|
||||
impl CanvasPaintThread {
|
||||
fn new(
|
||||
compositor_api: CrossProcessCompositorApi,
|
||||
system_font_service: Arc<SystemFontServiceProxy>,
|
||||
resource_threads: ResourceThreads,
|
||||
) -> CanvasPaintThread {
|
||||
fn new(compositor_api: CrossProcessCompositorApi) -> CanvasPaintThread {
|
||||
CanvasPaintThread {
|
||||
canvases: HashMap::new(),
|
||||
next_canvas_id: CanvasId(0),
|
||||
compositor_api: compositor_api.clone(),
|
||||
font_context: Arc::new(FontContext::new(
|
||||
system_font_service,
|
||||
compositor_api,
|
||||
resource_threads,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,8 +38,6 @@ impl CanvasPaintThread {
|
|||
/// communicate with it.
|
||||
pub fn start(
|
||||
compositor_api: CrossProcessCompositorApi,
|
||||
system_font_service: Arc<SystemFontServiceProxy>,
|
||||
resource_threads: ResourceThreads,
|
||||
) -> (Sender<ConstellationCanvasMsg>, IpcSender<CanvasMsg>) {
|
||||
let (ipc_sender, ipc_receiver) = ipc::channel::<CanvasMsg>().unwrap();
|
||||
let msg_receiver = ROUTER.route_ipc_receiver_to_new_crossbeam_receiver(ipc_receiver);
|
||||
|
@ -61,7 +46,7 @@ impl CanvasPaintThread {
|
|||
.name("Canvas".to_owned())
|
||||
.spawn(move || {
|
||||
let mut canvas_paint_thread = CanvasPaintThread::new(
|
||||
compositor_api, system_font_service, resource_threads);
|
||||
compositor_api);
|
||||
loop {
|
||||
select! {
|
||||
recv(msg_receiver) -> msg => {
|
||||
|
@ -108,7 +93,7 @@ impl CanvasPaintThread {
|
|||
let canvas_id = self.next_canvas_id;
|
||||
self.next_canvas_id.0 += 1;
|
||||
|
||||
let canvas = Canvas::new(size, self.compositor_api.clone(), self.font_context.clone())?;
|
||||
let canvas = Canvas::new(size, self.compositor_api.clone())?;
|
||||
let image_key = canvas.image_key();
|
||||
self.canvases.insert(canvas_id, canvas);
|
||||
|
||||
|
@ -122,25 +107,17 @@ impl CanvasPaintThread {
|
|||
fn process_canvas_2d_message(&mut self, message: Canvas2dMsg, canvas_id: CanvasId) {
|
||||
match message {
|
||||
Canvas2dMsg::FillText(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
max_width,
|
||||
style,
|
||||
is_rtl,
|
||||
text_options,
|
||||
text_bounds,
|
||||
text_runs,
|
||||
fill_or_stroke_style,
|
||||
shadow_options,
|
||||
composition_options,
|
||||
transform,
|
||||
) => {
|
||||
self.canvas(canvas_id).fill_text(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
max_width,
|
||||
is_rtl,
|
||||
style,
|
||||
text_options,
|
||||
text_bounds,
|
||||
text_runs,
|
||||
fill_or_stroke_style,
|
||||
shadow_options,
|
||||
composition_options,
|
||||
transform,
|
||||
|
@ -268,10 +245,6 @@ impl CanvasPaintThread {
|
|||
transform,
|
||||
);
|
||||
},
|
||||
Canvas2dMsg::MeasureText(text, sender, text_options) => {
|
||||
let metrics = self.canvas(canvas_id).measure_text(text, text_options);
|
||||
sender.send(metrics).unwrap();
|
||||
},
|
||||
Canvas2dMsg::GetImageData(dest_rect, sender) => {
|
||||
let snapshot = self.canvas(canvas_id).read_pixels(dest_rect);
|
||||
sender.send(snapshot.as_ipc()).unwrap();
|
||||
|
@ -302,27 +275,17 @@ enum Canvas {
|
|||
}
|
||||
|
||||
impl Canvas {
|
||||
fn new(
|
||||
size: Size2D<u64>,
|
||||
compositor_api: CrossProcessCompositorApi,
|
||||
font_context: Arc<FontContext>,
|
||||
) -> Option<Self> {
|
||||
fn new(size: Size2D<u64>, compositor_api: CrossProcessCompositorApi) -> Option<Self> {
|
||||
match servo_config::pref!(dom_canvas_backend)
|
||||
.to_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
#[cfg(feature = "vello_cpu")]
|
||||
"" | "auto" | "vello_cpu" => Some(Self::VelloCPU(CanvasData::new(
|
||||
size,
|
||||
compositor_api,
|
||||
font_context,
|
||||
))),
|
||||
"" | "auto" | "vello_cpu" => {
|
||||
Some(Self::VelloCPU(CanvasData::new(size, compositor_api)))
|
||||
},
|
||||
#[cfg(feature = "vello")]
|
||||
"" | "auto" | "vello" => Some(Self::Vello(CanvasData::new(
|
||||
size,
|
||||
compositor_api,
|
||||
font_context,
|
||||
))),
|
||||
"" | "auto" | "vello" => Some(Self::Vello(CanvasData::new(size, compositor_api))),
|
||||
s => {
|
||||
warn!("Unknown 2D canvas backend: `{s}`");
|
||||
None
|
||||
|
@ -348,16 +311,11 @@ impl Canvas {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn fill_text(
|
||||
&mut self,
|
||||
text: String,
|
||||
x: f64,
|
||||
y: f64,
|
||||
max_width: Option<f64>,
|
||||
is_rtl: bool,
|
||||
style: FillOrStrokeStyle,
|
||||
text_options: TextOptions,
|
||||
text_bounds: Rect<f64>,
|
||||
text_runs: Vec<TextRun>,
|
||||
fill_or_stroke_style: FillOrStrokeStyle,
|
||||
shadow_options: ShadowOptions,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
|
@ -365,26 +323,18 @@ impl Canvas {
|
|||
match self {
|
||||
#[cfg(feature = "vello")]
|
||||
Canvas::Vello(canvas_data) => canvas_data.fill_text(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
max_width,
|
||||
is_rtl,
|
||||
style,
|
||||
text_options,
|
||||
text_bounds,
|
||||
text_runs,
|
||||
fill_or_stroke_style,
|
||||
shadow_options,
|
||||
composition_options,
|
||||
transform,
|
||||
),
|
||||
#[cfg(feature = "vello_cpu")]
|
||||
Canvas::VelloCPU(canvas_data) => canvas_data.fill_text(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
max_width,
|
||||
is_rtl,
|
||||
style,
|
||||
text_options,
|
||||
text_bounds,
|
||||
text_runs,
|
||||
fill_or_stroke_style,
|
||||
shadow_options,
|
||||
composition_options,
|
||||
transform,
|
||||
|
@ -558,15 +508,6 @@ impl Canvas {
|
|||
}
|
||||
}
|
||||
|
||||
fn measure_text(&mut self, text: String, text_options: TextOptions) -> TextMetrics {
|
||||
match self {
|
||||
#[cfg(feature = "vello")]
|
||||
Canvas::Vello(canvas_data) => canvas_data.measure_text(text, text_options),
|
||||
#[cfg(feature = "vello_cpu")]
|
||||
Canvas::VelloCPU(canvas_data) => canvas_data.measure_text(text, text_options),
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_path(&mut self, path: &Path, fill_rule: FillRule, transform: Transform2D<f64>) {
|
||||
match self {
|
||||
#[cfg(feature = "vello")]
|
||||
|
|
|
@ -18,15 +18,14 @@ use std::rc::Rc;
|
|||
|
||||
use canvas_traits::canvas::{
|
||||
CompositionOptions, CompositionOrBlending, CompositionStyle, FillOrStrokeStyle, FillRule,
|
||||
LineOptions, Path, ShadowOptions,
|
||||
LineOptions, Path, ShadowOptions, TextRun,
|
||||
};
|
||||
use compositing_traits::SerializableImageData;
|
||||
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
|
||||
use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods as _};
|
||||
use fonts::FontIdentifier;
|
||||
use ipc_channel::ipc::IpcSharedMemory;
|
||||
use kurbo::Shape as _;
|
||||
use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
|
||||
use range::Range;
|
||||
use vello::wgpu::{
|
||||
BackendOptions, Backends, Buffer, BufferDescriptor, BufferUsages, COPY_BYTES_PER_ROW_ALIGNMENT,
|
||||
CommandEncoderDescriptor, Device, Extent3d, Instance, InstanceDescriptor, InstanceFlags,
|
||||
|
@ -38,7 +37,7 @@ use vello::{kurbo, peniko};
|
|||
use webrender_api::{ImageDescriptor, ImageDescriptorFlags};
|
||||
|
||||
use crate::backend::{Convert as _, GenericDrawTarget};
|
||||
use crate::canvas_data::{Filter, TextRun};
|
||||
use crate::canvas_data::Filter;
|
||||
|
||||
thread_local! {
|
||||
/// The shared font cache used by all canvases that render on a thread.
|
||||
|
@ -381,7 +380,6 @@ impl GenericDrawTarget for VelloDrawTarget {
|
|||
fn fill_text(
|
||||
&mut self,
|
||||
text_runs: Vec<TextRun>,
|
||||
start: Point2D<f32>,
|
||||
style: FillOrStrokeStyle,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
|
@ -390,24 +388,19 @@ impl GenericDrawTarget for VelloDrawTarget {
|
|||
let pattern = convert_to_brush(style, composition_options);
|
||||
let transform = transform.cast().into();
|
||||
self.with_composition(composition_options.composition_operation, |self_| {
|
||||
let mut advance = 0.;
|
||||
for run in text_runs.iter() {
|
||||
let glyphs = &run.glyphs;
|
||||
|
||||
let template = &run.font.template;
|
||||
|
||||
for text_run in text_runs.iter() {
|
||||
SHARED_FONT_CACHE.with(|font_cache| {
|
||||
let identifier = template.identifier();
|
||||
if !font_cache.borrow().contains_key(&identifier) {
|
||||
let Ok(font) = run.font.font_data_and_index() else {
|
||||
let identifier = &text_run.font.identifier;
|
||||
if !font_cache.borrow().contains_key(identifier) {
|
||||
let Some(font_data_and_index) = text_run.font.font_data_and_index() else {
|
||||
return;
|
||||
};
|
||||
let font = font.clone().convert();
|
||||
let font = font_data_and_index.convert();
|
||||
font_cache.borrow_mut().insert(identifier.clone(), font);
|
||||
}
|
||||
|
||||
let font_cache = font_cache.borrow();
|
||||
let Some(font) = font_cache.get(&identifier) else {
|
||||
let Some(font) = font_cache.get(identifier) else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
@ -416,21 +409,16 @@ impl GenericDrawTarget for VelloDrawTarget {
|
|||
.draw_glyphs(font)
|
||||
.transform(transform)
|
||||
.brush(&pattern)
|
||||
.font_size(run.font.descriptor.pt_size.to_f32_px())
|
||||
.font_size(text_run.pt_size)
|
||||
.draw(
|
||||
peniko::Fill::NonZero,
|
||||
glyphs
|
||||
.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), glyphs.len()))
|
||||
.map(|glyph| {
|
||||
let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
|
||||
let x = advance + start.x + glyph_offset.x.to_f32_px();
|
||||
let y = start.y + glyph_offset.y.to_f32_px();
|
||||
advance += glyph.advance().to_f32_px();
|
||||
vello::Glyph {
|
||||
id: glyph.id(),
|
||||
x,
|
||||
y,
|
||||
}
|
||||
text_run
|
||||
.glyphs_and_positions
|
||||
.iter()
|
||||
.map(|glyph_and_position| vello::Glyph {
|
||||
id: glyph_and_position.id,
|
||||
x: glyph_and_position.point.x,
|
||||
y: glyph_and_position.point.y,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -8,20 +8,19 @@ use std::sync::Arc;
|
|||
|
||||
use canvas_traits::canvas::{
|
||||
CompositionOptions, CompositionOrBlending, CompositionStyle, FillOrStrokeStyle, FillRule,
|
||||
LineOptions, Path, ShadowOptions,
|
||||
LineOptions, Path, ShadowOptions, TextRun,
|
||||
};
|
||||
use compositing_traits::SerializableImageData;
|
||||
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
|
||||
use fonts::{ByteIndex, FontIdentifier, FontTemplateRefMethods as _};
|
||||
use fonts::FontIdentifier;
|
||||
use ipc_channel::ipc::IpcSharedMemory;
|
||||
use kurbo::Shape;
|
||||
use pixels::{Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
|
||||
use range::Range;
|
||||
use vello_cpu::{kurbo, peniko};
|
||||
use webrender_api::{ImageDescriptor, ImageDescriptorFlags};
|
||||
|
||||
use crate::backend::{Convert, GenericDrawTarget};
|
||||
use crate::canvas_data::{Filter, TextRun};
|
||||
use crate::canvas_data::Filter;
|
||||
|
||||
thread_local! {
|
||||
/// The shared font cache used by all canvases that render on a thread.
|
||||
|
@ -282,7 +281,6 @@ impl GenericDrawTarget for VelloCPUDrawTarget {
|
|||
fn fill_text(
|
||||
&mut self,
|
||||
text_runs: Vec<TextRun>,
|
||||
start: Point2D<f32>,
|
||||
style: FillOrStrokeStyle,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
|
@ -291,46 +289,32 @@ impl GenericDrawTarget for VelloCPUDrawTarget {
|
|||
self.ctx.set_paint(paint(style, composition_options.alpha));
|
||||
self.ctx.set_transform(transform.cast().into());
|
||||
self.with_composition(composition_options.composition_operation, |self_| {
|
||||
let mut advance = 0.;
|
||||
for run in text_runs.iter() {
|
||||
let glyphs = &run.glyphs;
|
||||
|
||||
let template = &run.font.template;
|
||||
|
||||
for text_run in text_runs.iter() {
|
||||
SHARED_FONT_CACHE.with(|font_cache| {
|
||||
let identifier = template.identifier();
|
||||
if !font_cache.borrow().contains_key(&identifier) {
|
||||
let Ok(font) = run.font.font_data_and_index() else {
|
||||
let identifier = &text_run.font.identifier;
|
||||
if !font_cache.borrow().contains_key(identifier) {
|
||||
let Some(font_data_and_index) = text_run.font.font_data_and_index() else {
|
||||
return;
|
||||
};
|
||||
let font = font.clone().convert();
|
||||
let font = font_data_and_index.convert();
|
||||
font_cache.borrow_mut().insert(identifier.clone(), font);
|
||||
}
|
||||
|
||||
let font_cache = font_cache.borrow();
|
||||
let Some(font) = font_cache.get(&identifier) else {
|
||||
let Some(font) = font_cache.get(identifier) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self_
|
||||
.ctx
|
||||
.glyph_run(font)
|
||||
.font_size(run.font.descriptor.pt_size.to_f32_px())
|
||||
.fill_glyphs(
|
||||
glyphs
|
||||
.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), glyphs.len()))
|
||||
.map(|glyph| {
|
||||
let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
|
||||
let x = advance + start.x + glyph_offset.x.to_f32_px();
|
||||
let y = start.y + glyph_offset.y.to_f32_px();
|
||||
advance += glyph.advance().to_f32_px();
|
||||
vello_cpu::Glyph {
|
||||
id: glyph.id(),
|
||||
x,
|
||||
y,
|
||||
}
|
||||
}),
|
||||
);
|
||||
.font_size(text_run.pt_size)
|
||||
.fill_glyphs(text_run.glyphs_and_positions.iter().map(
|
||||
|glyph_and_position| vello_cpu::Glyph {
|
||||
id: glyph_and_position.id,
|
||||
x: glyph_and_position.point.x,
|
||||
y: glyph_and_position.point.y,
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5617,10 +5617,6 @@ where
|
|||
}
|
||||
|
||||
fn create_canvas_paint_thread(&self) -> (Sender<ConstellationCanvasMsg>, IpcSender<CanvasMsg>) {
|
||||
CanvasPaintThread::start(
|
||||
self.compositor_proxy.cross_process_compositor_api.clone(),
|
||||
self.system_font_service.clone(),
|
||||
self.public_resource_threads.clone(),
|
||||
)
|
||||
CanvasPaintThread::start(self.compositor_proxy.cross_process_compositor_api.clone())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,6 +129,7 @@ time = { workspace = true }
|
|||
timers = { path = "../timers" }
|
||||
tracing = { workspace = true, optional = true }
|
||||
unicode-bidi = { workspace = true }
|
||||
unicode-script = { workspace = true }
|
||||
unicode-segmentation = { workspace = true }
|
||||
url = { workspace = true }
|
||||
urlpattern = { workspace = true }
|
||||
|
|
|
@ -7,22 +7,28 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use app_units::Au;
|
||||
use canvas_traits::canvas::{
|
||||
Canvas2dMsg, CanvasId, CanvasMsg, CompositionOptions, CompositionOrBlending, Direction,
|
||||
FillOrStrokeStyle, FillRule, LineCapStyle, LineJoinStyle, LineOptions, LinearGradientStyle,
|
||||
Path, RadialGradientStyle, RepetitionStyle, ShadowOptions, TextAlign, TextBaseline,
|
||||
TextMetrics as CanvasTextMetrics, TextOptions,
|
||||
Canvas2dMsg, CanvasFont, CanvasId, CanvasMsg, CompositionOptions, CompositionOrBlending,
|
||||
FillOrStrokeStyle, FillRule, GlyphAndPosition, LineCapStyle, LineJoinStyle, LineOptions,
|
||||
LinearGradientStyle, Path, RadialGradientStyle, RepetitionStyle, ShadowOptions, TextRun,
|
||||
};
|
||||
use constellation_traits::ScriptToConstellationMessage;
|
||||
use cssparser::color::clamp_unit_f32;
|
||||
use cssparser::{Parser, ParserInput};
|
||||
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
|
||||
use euclid::vec2;
|
||||
use euclid::{Vector2D, vec2};
|
||||
use fonts::{
|
||||
ByteIndex, FontBaseline, FontContext, FontGroup, FontIdentifier, FontMetrics, FontRef,
|
||||
LAST_RESORT_GLYPH_ADVANCE, ShapingFlags, ShapingOptions,
|
||||
};
|
||||
use ipc_channel::ipc::{self, IpcSender};
|
||||
use net_traits::image_cache::{ImageCache, ImageResponse};
|
||||
use net_traits::request::CorsSettings;
|
||||
use pixels::{PixelFormat, Snapshot, SnapshotAlphaMode, SnapshotPixelFormat};
|
||||
use profile_traits::ipc as profiled_ipc;
|
||||
use range::Range;
|
||||
use servo_arc::Arc as ServoArc;
|
||||
use servo_url::{ImmutableOrigin, ServoUrl};
|
||||
use style::color::{AbsoluteColor, ColorFlags, ColorSpace};
|
||||
use style::context::QuirksMode;
|
||||
|
@ -34,6 +40,7 @@ use style::values::computed::font::FontStyle;
|
|||
use style::values::specified::color::Color;
|
||||
use style_traits::values::ToCss;
|
||||
use style_traits::{CssWriter, ParsingMode};
|
||||
use unicode_script::Script;
|
||||
use url::Url;
|
||||
use webrender_api::ImageKey;
|
||||
|
||||
|
@ -68,6 +75,9 @@ use crate::dom::paintworkletglobalscope::PaintWorkletGlobalScope;
|
|||
use crate::dom::textmetrics::TextMetrics;
|
||||
use crate::script_runtime::CanGc;
|
||||
|
||||
const HANGING_BASELINE_DEFAULT: f64 = 0.8;
|
||||
const IDEOGRAPHIC_BASELINE_DEFAULT: f64 = 0.5;
|
||||
|
||||
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
|
||||
#[derive(Clone, JSTraceable, MallocSizeOf)]
|
||||
#[allow(dead_code)]
|
||||
|
@ -112,13 +122,11 @@ pub(crate) struct CanvasContextState {
|
|||
#[no_trace]
|
||||
shadow_color: AbsoluteColor,
|
||||
#[no_trace]
|
||||
font_style: Option<Font>,
|
||||
#[no_trace]
|
||||
text_align: TextAlign,
|
||||
#[no_trace]
|
||||
text_baseline: TextBaseline,
|
||||
#[no_trace]
|
||||
direction: Direction,
|
||||
#[conditional_malloc_size_of]
|
||||
font_style: Option<ServoArc<Font>>,
|
||||
text_align: CanvasTextAlign,
|
||||
text_baseline: CanvasTextBaseline,
|
||||
direction: CanvasDirection,
|
||||
/// The number of clips pushed onto the context while in this state.
|
||||
/// When restoring old state, same number of clips will be popped to restore state.
|
||||
clips_pushed: usize,
|
||||
|
@ -144,26 +152,15 @@ impl CanvasContextState {
|
|||
shadow_blur: 0.0,
|
||||
shadow_color: AbsoluteColor::TRANSPARENT_BLACK,
|
||||
font_style: None,
|
||||
text_align: Default::default(),
|
||||
text_baseline: Default::default(),
|
||||
direction: Default::default(),
|
||||
text_align: CanvasTextAlign::Start,
|
||||
text_baseline: CanvasTextBaseline::Alphabetic,
|
||||
direction: CanvasDirection::Inherit,
|
||||
line_dash: Vec::new(),
|
||||
line_dash_offset: 0.0,
|
||||
clips_pushed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn text_options(&self) -> TextOptions {
|
||||
TextOptions {
|
||||
font: self
|
||||
.font_style
|
||||
.as_ref()
|
||||
.map(|font| servo_arc::Arc::new(font.clone())),
|
||||
align: self.text_align,
|
||||
baseline: self.text_baseline,
|
||||
}
|
||||
}
|
||||
|
||||
fn composition_options(&self) -> CompositionOptions {
|
||||
CompositionOptions {
|
||||
alpha: self.global_alpha,
|
||||
|
@ -1378,6 +1375,7 @@ impl CanvasState {
|
|||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-filltext
|
||||
pub(crate) fn fill_text(
|
||||
&self,
|
||||
global_scope: &GlobalScope,
|
||||
canvas: Option<&HTMLCanvasElement>,
|
||||
text: DOMString,
|
||||
x: f64,
|
||||
|
@ -1390,32 +1388,26 @@ impl CanvasState {
|
|||
if max_width.is_some_and(|max_width| !max_width.is_finite() || max_width <= 0.) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.state.borrow().font_style.is_none() {
|
||||
self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into())
|
||||
}
|
||||
// This may be `None` if if this is offscreen canvas, in which case just use
|
||||
// the initial values for the text style.
|
||||
let size = self.font_style().font_size.computed_size().px() as f64;
|
||||
|
||||
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(),
|
||||
x,
|
||||
y,
|
||||
self.fill_text_with_size(
|
||||
global_scope,
|
||||
text.str(),
|
||||
Point2D::new(x, y),
|
||||
size,
|
||||
max_width,
|
||||
style,
|
||||
is_rtl,
|
||||
self.state.borrow().text_options(),
|
||||
self.state.borrow().shadow_options(),
|
||||
self.state.borrow().composition_options(),
|
||||
self.state.borrow().transform,
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#textmetrics
|
||||
/// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>
|
||||
/// <https://html.spec.whatwg.org/multipage/#dom-context-2d-measuretext>
|
||||
/// <https://html.spec.whatwg.org/multipage/#textmetrics>
|
||||
pub(crate) fn measure_text(
|
||||
&self,
|
||||
global: &GlobalScope,
|
||||
|
@ -1423,40 +1415,86 @@ impl CanvasState {
|
|||
text: DOMString,
|
||||
can_gc: CanGc,
|
||||
) -> DomRoot<TextMetrics> {
|
||||
// > Step 1: If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.0
|
||||
// Max width is not provided for `measureText()`.
|
||||
|
||||
// > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
|
||||
let text = replace_ascii_whitespace(text.str());
|
||||
|
||||
// > Step 3: Let font be the current font of target, as given by that object's font
|
||||
// > attribute.
|
||||
if self.state.borrow().font_style.is_none() {
|
||||
self.set_font(canvas, CanvasContextState::DEFAULT_FONT_STYLE.into());
|
||||
}
|
||||
|
||||
let metrics = {
|
||||
if !self.is_paintable() {
|
||||
CanvasTextMetrics::default()
|
||||
} else {
|
||||
let (sender, receiver) = ipc::channel::<CanvasTextMetrics>().unwrap();
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::MeasureText(
|
||||
text.into(),
|
||||
sender,
|
||||
self.state.borrow().text_options(),
|
||||
));
|
||||
receiver
|
||||
.recv()
|
||||
.expect("Failed to receive response from canvas paint thread")
|
||||
}
|
||||
let Some(font_context) = global.font_context() else {
|
||||
warn!("Tried to paint to a canvas of GlobalScope without a FontContext.");
|
||||
return TextMetrics::default(global, can_gc);
|
||||
};
|
||||
|
||||
let font_style = self.font_style();
|
||||
let font_group = font_context.font_group(font_style.clone());
|
||||
let mut font_group = font_group.write();
|
||||
let font = font_group.first(font_context).expect("couldn't find font");
|
||||
let ascent = font.metrics.ascent.to_f64_px();
|
||||
let descent = font.metrics.descent.to_f64_px();
|
||||
let runs = self.build_unshaped_text_runs(font_context, &text, &mut font_group);
|
||||
|
||||
let mut total_advance = 0.0;
|
||||
let shaped_runs: Vec<_> = runs
|
||||
.into_iter()
|
||||
.filter_map(|unshaped_text_run| {
|
||||
let text_run = unshaped_text_run.into_shaped_text_run(total_advance)?;
|
||||
total_advance += text_run.advance;
|
||||
Some(text_run)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let bounding_box = shaped_runs
|
||||
.iter()
|
||||
.map(|text_run| text_run.bounds)
|
||||
.reduce(|a, b| a.union(&b))
|
||||
.unwrap_or_default();
|
||||
|
||||
let baseline = font.baseline().unwrap_or_else(|| FontBaseline {
|
||||
hanging_baseline: (ascent * HANGING_BASELINE_DEFAULT) as f32,
|
||||
ideographic_baseline: (-descent * IDEOGRAPHIC_BASELINE_DEFAULT) as f32,
|
||||
alphabetic_baseline: 0.,
|
||||
});
|
||||
let ideographic_baseline = baseline.ideographic_baseline as f64;
|
||||
let alphabetic_baseline = baseline.alphabetic_baseline as f64;
|
||||
let hanging_baseline = baseline.hanging_baseline as f64;
|
||||
|
||||
let state = self.state.borrow();
|
||||
let anchor_x = match state.text_align {
|
||||
CanvasTextAlign::End => total_advance,
|
||||
CanvasTextAlign::Center => total_advance / 2.,
|
||||
CanvasTextAlign::Right => total_advance,
|
||||
_ => 0.,
|
||||
} as f64;
|
||||
let anchor_y = match state.text_baseline {
|
||||
CanvasTextBaseline::Top => ascent,
|
||||
CanvasTextBaseline::Hanging => hanging_baseline,
|
||||
CanvasTextBaseline::Ideographic => ideographic_baseline,
|
||||
CanvasTextBaseline::Middle => (ascent - descent) / 2.,
|
||||
CanvasTextBaseline::Alphabetic => alphabetic_baseline,
|
||||
CanvasTextBaseline::Bottom => -descent,
|
||||
};
|
||||
|
||||
TextMetrics::new(
|
||||
global,
|
||||
metrics.width.into(),
|
||||
metrics.actual_boundingbox_left.into(),
|
||||
metrics.actual_boundingbox_right.into(),
|
||||
metrics.font_boundingbox_ascent.into(),
|
||||
metrics.font_boundingbox_descent.into(),
|
||||
metrics.actual_boundingbox_ascent.into(),
|
||||
metrics.actual_boundingbox_descent.into(),
|
||||
metrics.em_height_ascent.into(),
|
||||
metrics.em_height_descent.into(),
|
||||
metrics.hanging_baseline.into(),
|
||||
metrics.alphabetic_baseline.into(),
|
||||
metrics.ideographic_baseline.into(),
|
||||
total_advance as f64,
|
||||
anchor_x - bounding_box.min_x(),
|
||||
bounding_box.max_x() - anchor_x,
|
||||
bounding_box.max_y() - anchor_y,
|
||||
anchor_y - bounding_box.min_y(),
|
||||
ascent - anchor_y,
|
||||
descent + anchor_y,
|
||||
ascent - anchor_y,
|
||||
descent + anchor_y,
|
||||
hanging_baseline - anchor_y,
|
||||
alphabetic_baseline - anchor_y,
|
||||
ideographic_baseline - anchor_y,
|
||||
can_gc,
|
||||
)
|
||||
}
|
||||
|
@ -1469,11 +1507,21 @@ impl CanvasState {
|
|||
};
|
||||
let node = canvas.upcast::<Node>();
|
||||
let window = canvas.owner_window();
|
||||
let resolved_font_style = match window.resolved_font_style_query(node, value.to_string()) {
|
||||
Some(value) => value,
|
||||
None => return, // syntax error
|
||||
|
||||
let Some(resolved_font_style) = window.resolved_font_style_query(node, value.to_string())
|
||||
else {
|
||||
// This will happen when there is a syntax error.
|
||||
return;
|
||||
};
|
||||
self.state.borrow_mut().font_style = Some((*resolved_font_style).clone());
|
||||
self.state.borrow_mut().font_style = Some(resolved_font_style);
|
||||
}
|
||||
|
||||
fn font_style(&self) -> ServoArc<Font> {
|
||||
self.state
|
||||
.borrow()
|
||||
.font_style
|
||||
.clone()
|
||||
.unwrap_or_else(|| ServoArc::new(Font::initial_values()))
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-font
|
||||
|
@ -1490,67 +1538,30 @@ impl CanvasState {
|
|||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-textalign
|
||||
pub(crate) fn text_align(&self) -> CanvasTextAlign {
|
||||
match self.state.borrow().text_align {
|
||||
TextAlign::Start => CanvasTextAlign::Start,
|
||||
TextAlign::End => CanvasTextAlign::End,
|
||||
TextAlign::Left => CanvasTextAlign::Left,
|
||||
TextAlign::Right => CanvasTextAlign::Right,
|
||||
TextAlign::Center => CanvasTextAlign::Center,
|
||||
}
|
||||
self.state.borrow().text_align
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-textalign
|
||||
pub(crate) fn set_text_align(&self, value: CanvasTextAlign) {
|
||||
let text_align = match value {
|
||||
CanvasTextAlign::Start => TextAlign::Start,
|
||||
CanvasTextAlign::End => TextAlign::End,
|
||||
CanvasTextAlign::Left => TextAlign::Left,
|
||||
CanvasTextAlign::Right => TextAlign::Right,
|
||||
CanvasTextAlign::Center => TextAlign::Center,
|
||||
};
|
||||
self.state.borrow_mut().text_align = text_align;
|
||||
self.state.borrow_mut().text_align = value;
|
||||
}
|
||||
|
||||
pub(crate) fn text_baseline(&self) -> CanvasTextBaseline {
|
||||
match self.state.borrow().text_baseline {
|
||||
TextBaseline::Top => CanvasTextBaseline::Top,
|
||||
TextBaseline::Hanging => CanvasTextBaseline::Hanging,
|
||||
TextBaseline::Middle => CanvasTextBaseline::Middle,
|
||||
TextBaseline::Alphabetic => CanvasTextBaseline::Alphabetic,
|
||||
TextBaseline::Ideographic => CanvasTextBaseline::Ideographic,
|
||||
TextBaseline::Bottom => CanvasTextBaseline::Bottom,
|
||||
}
|
||||
self.state.borrow().text_baseline
|
||||
}
|
||||
|
||||
pub(crate) fn set_text_baseline(&self, value: CanvasTextBaseline) {
|
||||
let text_baseline = match value {
|
||||
CanvasTextBaseline::Top => TextBaseline::Top,
|
||||
CanvasTextBaseline::Hanging => TextBaseline::Hanging,
|
||||
CanvasTextBaseline::Middle => TextBaseline::Middle,
|
||||
CanvasTextBaseline::Alphabetic => TextBaseline::Alphabetic,
|
||||
CanvasTextBaseline::Ideographic => TextBaseline::Ideographic,
|
||||
CanvasTextBaseline::Bottom => TextBaseline::Bottom,
|
||||
};
|
||||
self.state.borrow_mut().text_baseline = text_baseline;
|
||||
self.state.borrow_mut().text_baseline = value;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-direction
|
||||
pub(crate) fn direction(&self) -> CanvasDirection {
|
||||
match self.state.borrow().direction {
|
||||
Direction::Ltr => CanvasDirection::Ltr,
|
||||
Direction::Rtl => CanvasDirection::Rtl,
|
||||
Direction::Inherit => CanvasDirection::Inherit,
|
||||
}
|
||||
self.state.borrow().direction
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-direction
|
||||
pub(crate) fn set_direction(&self, value: CanvasDirection) {
|
||||
let direction = match value {
|
||||
CanvasDirection::Ltr => Direction::Ltr,
|
||||
CanvasDirection::Rtl => Direction::Rtl,
|
||||
CanvasDirection::Inherit => Direction::Inherit,
|
||||
};
|
||||
self.state.borrow_mut().direction = direction;
|
||||
self.state.borrow_mut().direction = value;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-linewidth
|
||||
|
@ -2177,6 +2188,175 @@ impl CanvasState {
|
|||
.ellipse(x, y, rx, ry, rotation, start, end, ccw)
|
||||
.map_err(|_| Error::IndexSize)
|
||||
}
|
||||
|
||||
fn fill_text_with_size(
|
||||
&self,
|
||||
global_scope: &GlobalScope,
|
||||
text: &str,
|
||||
origin: Point2D<f64>,
|
||||
size: f64,
|
||||
max_width: Option<f64>,
|
||||
) {
|
||||
let Some(font_context) = global_scope.font_context() else {
|
||||
warn!("Tried to paint to a canvas of GlobalScope without a FontContext.");
|
||||
return;
|
||||
};
|
||||
|
||||
// > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
|
||||
let text = replace_ascii_whitespace(text);
|
||||
|
||||
// > Step 3: Let font be the current font of target, as given by that object's font
|
||||
// > attribute.
|
||||
let font_style = self.font_style();
|
||||
let font_group = font_context.font_group_with_size(font_style, Au::from_f64_px(size));
|
||||
let mut font_group = font_group.write();
|
||||
let Some(first_font) = font_group.first(font_context) else {
|
||||
warn!("Could not render canvas text, because there was no first font.");
|
||||
return;
|
||||
};
|
||||
|
||||
let runs = self.build_unshaped_text_runs(font_context, &text, &mut font_group);
|
||||
|
||||
// TODO: This doesn't do any kind of line layout at all. In particular, there needs
|
||||
// to be some alignment along a baseline and also support for bidi text.
|
||||
let mut total_advance = 0.0;
|
||||
let mut shaped_runs: Vec<_> = runs
|
||||
.into_iter()
|
||||
.filter_map(|unshaped_text_run| {
|
||||
let text_run = unshaped_text_run.into_shaped_text_run(total_advance)?;
|
||||
total_advance += text_run.advance;
|
||||
Some(text_run)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// > Step 6: If maxWidth was provided and the hypothetical width of the inline box in the
|
||||
// > hypothetical line box is greater than maxWidth CSS pixels, then change font to have a
|
||||
// > more condensed font (if one is available or if a reasonably readable one can be
|
||||
// > synthesized by applying a horizontal scale factor to the font) or a smaller font, and
|
||||
// > return to the previous step.
|
||||
//
|
||||
// TODO: We only try decreasing the font size here. Eventually it would make sense to use
|
||||
// other methods to try to decrease the size, such as finding a narrower font or decreasing
|
||||
// spacing.
|
||||
let total_advance = total_advance as f64;
|
||||
if let Some(max_width) = max_width {
|
||||
let new_size = (max_width / total_advance * size).floor().max(5.);
|
||||
if total_advance > max_width && new_size != size {
|
||||
self.fill_text_with_size(global_scope, &text, origin, new_size, Some(max_width));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// > Step 7: Find the anchor point for the line of text.
|
||||
let start =
|
||||
self.find_anchor_point_for_line_of_text(origin, &first_font.metrics, total_advance);
|
||||
|
||||
// > Step 8: Let result be an array constructed by iterating over each glyph in the inline box
|
||||
// > from left to right (if any), adding to the array, for each glyph, the shape of the glyph
|
||||
// > as it is in the inline box, positioned on a coordinate space using CSS pixels with its
|
||||
// > origin is at the anchor point.
|
||||
let mut bounds = None;
|
||||
for text_run in shaped_runs.iter_mut() {
|
||||
for glyph_and_position in text_run.glyphs_and_positions.iter_mut() {
|
||||
glyph_and_position.point += Vector2D::new(start.x as f32, start.y as f32);
|
||||
}
|
||||
bounds
|
||||
.get_or_insert(text_run.bounds)
|
||||
.union(&text_run.bounds);
|
||||
}
|
||||
|
||||
println!("bounds: {bounds:?}");
|
||||
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::FillText(
|
||||
bounds
|
||||
.unwrap_or_default()
|
||||
.translate(start.to_vector().cast_unit()),
|
||||
shaped_runs,
|
||||
self.state.borrow().fill_style.to_fill_or_stroke_style(),
|
||||
self.state.borrow().shadow_options(),
|
||||
self.state.borrow().composition_options(),
|
||||
self.state.borrow().transform,
|
||||
));
|
||||
}
|
||||
|
||||
fn build_unshaped_text_runs<'text>(
|
||||
&self,
|
||||
font_context: &FontContext,
|
||||
text: &'text str,
|
||||
font_group: &mut FontGroup,
|
||||
) -> Vec<UnshapedTextRun<'text>> {
|
||||
let mut runs = Vec::new();
|
||||
let mut current_text_run = UnshapedTextRun::default();
|
||||
let mut current_text_run_start_index = 0;
|
||||
|
||||
for (index, character) in text.char_indices() {
|
||||
// TODO: This should ultimately handle emoji variation selectors, but raqote does not yet
|
||||
// have support for color glyphs.
|
||||
let script = Script::from(character);
|
||||
let font = font_group.find_by_codepoint(font_context, character, None, None);
|
||||
|
||||
if !current_text_run.script_and_font_compatible(script, &font) {
|
||||
let previous_text_run = std::mem::replace(
|
||||
&mut current_text_run,
|
||||
UnshapedTextRun {
|
||||
font: font.clone(),
|
||||
script,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
current_text_run_start_index = index;
|
||||
runs.push(previous_text_run)
|
||||
}
|
||||
|
||||
current_text_run.string =
|
||||
&text[current_text_run_start_index..index + character.len_utf8()];
|
||||
}
|
||||
|
||||
runs.push(current_text_run);
|
||||
runs
|
||||
}
|
||||
|
||||
/// Find the *anchor_point* for the given parameters of a line of text.
|
||||
/// See <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>.
|
||||
fn find_anchor_point_for_line_of_text(
|
||||
&self,
|
||||
origin: Point2D<f64>,
|
||||
metrics: &FontMetrics,
|
||||
width: f64,
|
||||
) -> Point2D<f64> {
|
||||
let state = self.state.borrow();
|
||||
let is_rtl = match state.direction {
|
||||
CanvasDirection::Ltr => false,
|
||||
CanvasDirection::Rtl => true,
|
||||
CanvasDirection::Inherit => false, // TODO: resolve direction wrt to canvas element
|
||||
};
|
||||
|
||||
let text_align = match self.text_align() {
|
||||
CanvasTextAlign::Start if is_rtl => CanvasTextAlign::Right,
|
||||
CanvasTextAlign::Start => CanvasTextAlign::Left,
|
||||
CanvasTextAlign::End if is_rtl => CanvasTextAlign::Left,
|
||||
CanvasTextAlign::End => CanvasTextAlign::Right,
|
||||
text_align => text_align,
|
||||
};
|
||||
let anchor_x = match text_align {
|
||||
CanvasTextAlign::Center => -width / 2.,
|
||||
CanvasTextAlign::Right => -width,
|
||||
_ => 0.,
|
||||
};
|
||||
|
||||
let ascent = metrics.ascent.to_f64_px();
|
||||
let descent = metrics.descent.to_f64_px();
|
||||
let anchor_y = match self.text_baseline() {
|
||||
CanvasTextBaseline::Top => ascent,
|
||||
CanvasTextBaseline::Hanging => ascent * HANGING_BASELINE_DEFAULT,
|
||||
CanvasTextBaseline::Ideographic => -descent * IDEOGRAPHIC_BASELINE_DEFAULT,
|
||||
CanvasTextBaseline::Middle => (ascent - descent) / 2.,
|
||||
CanvasTextBaseline::Alphabetic => 0.,
|
||||
CanvasTextBaseline::Bottom => -descent,
|
||||
};
|
||||
|
||||
origin + Vector2D::new(anchor_x, anchor_y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CanvasState {
|
||||
|
@ -2187,6 +2367,89 @@ impl Drop for CanvasState {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct UnshapedTextRun<'a> {
|
||||
font: Option<FontRef>,
|
||||
script: Script,
|
||||
string: &'a str,
|
||||
}
|
||||
|
||||
impl UnshapedTextRun<'_> {
|
||||
fn script_and_font_compatible(&self, script: Script, other_font: &Option<FontRef>) -> bool {
|
||||
if self.script != script {
|
||||
return false;
|
||||
}
|
||||
|
||||
match (&self.font, other_font) {
|
||||
(Some(font_a), Some(font_b)) => font_a.identifier() == font_b.identifier(),
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_shaped_text_run(self, previous_advance: f32) -> Option<TextRun> {
|
||||
let font = self.font?;
|
||||
if self.string.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let word_spacing = Au::from_f64_px(
|
||||
font.glyph_index(' ')
|
||||
.map(|glyph_id| font.glyph_h_advance(glyph_id))
|
||||
.unwrap_or(LAST_RESORT_GLYPH_ADVANCE),
|
||||
);
|
||||
let options = ShapingOptions {
|
||||
letter_spacing: None,
|
||||
word_spacing,
|
||||
script: self.script,
|
||||
flags: ShapingFlags::empty(),
|
||||
};
|
||||
|
||||
let glyphs = font.shape_text(self.string, &options);
|
||||
|
||||
let mut advance = 0.0;
|
||||
let mut bounds = None;
|
||||
let glyphs_and_positions = glyphs
|
||||
.iter_glyphs_for_byte_range(&Range::new(ByteIndex(0), glyphs.len()))
|
||||
.map(|glyph| {
|
||||
let glyph_offset = glyph.offset().unwrap_or(Point2D::zero());
|
||||
let glyph_and_position = GlyphAndPosition {
|
||||
id: glyph.id(),
|
||||
point: Point2D::new(previous_advance + advance, glyph_offset.y.to_f32_px()),
|
||||
};
|
||||
|
||||
let glyph_bounds = font
|
||||
.typographic_bounds(glyph.id())
|
||||
.translate(Vector2D::new(advance + previous_advance, 0.0));
|
||||
bounds = Some(bounds.get_or_insert(glyph_bounds).union(&glyph_bounds));
|
||||
|
||||
advance += glyph.advance().to_f32_px();
|
||||
|
||||
glyph_and_position
|
||||
})
|
||||
.collect();
|
||||
|
||||
let identifier = font.identifier();
|
||||
let font_data = match &identifier {
|
||||
FontIdentifier::Local(_) => None,
|
||||
FontIdentifier::Web(_) => Some(font.font_data_and_index().ok()?),
|
||||
}
|
||||
.cloned();
|
||||
let canvas_font = CanvasFont {
|
||||
identifier,
|
||||
data: font_data,
|
||||
};
|
||||
|
||||
Some(TextRun {
|
||||
font: canvas_font,
|
||||
pt_size: font.descriptor.pt_size.to_f32_px(),
|
||||
glyphs_and_positions,
|
||||
advance,
|
||||
bounds: bounds.unwrap_or_default().cast(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_color(
|
||||
canvas: Option<&HTMLCanvasElement>,
|
||||
string: &str,
|
||||
|
@ -2334,3 +2597,12 @@ impl Convert<FillRule> for CanvasFillRule {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_ascii_whitespace(text: &str) -> String {
|
||||
text.chars()
|
||||
.map(|c| match c {
|
||||
' ' | '\t' | '\n' | '\r' | '\x0C' => '\x20',
|
||||
_ => c,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -328,8 +328,14 @@ impl CanvasRenderingContext2DMethods<crate::DomTypeHolder> for CanvasRenderingCo
|
|||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-filltext
|
||||
fn FillText(&self, text: DOMString, x: f64, y: f64, max_width: Option<f64>) {
|
||||
self.canvas_state
|
||||
.fill_text(self.canvas.canvas().as_deref(), text, x, y, max_width);
|
||||
self.canvas_state.fill_text(
|
||||
&self.global(),
|
||||
self.canvas.canvas().as_deref(),
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
max_width,
|
||||
);
|
||||
self.mark_as_dirty();
|
||||
}
|
||||
|
||||
|
|
|
@ -99,6 +99,28 @@ impl TextMetrics {
|
|||
can_gc,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn default(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Self> {
|
||||
reflect_dom_object(
|
||||
Box::new(Self {
|
||||
reflector_: Reflector::new(),
|
||||
width: Default::default(),
|
||||
actualBoundingBoxLeft: Default::default(),
|
||||
actualBoundingBoxRight: Default::default(),
|
||||
fontBoundingBoxAscent: Default::default(),
|
||||
fontBoundingBoxDescent: Default::default(),
|
||||
actualBoundingBoxAscent: Default::default(),
|
||||
actualBoundingBoxDescent: Default::default(),
|
||||
emHeightAscent: Default::default(),
|
||||
emHeightDescent: Default::default(),
|
||||
hangingBaseline: Default::default(),
|
||||
alphabeticBaseline: Default::default(),
|
||||
ideographicBaseline: Default::default(),
|
||||
}),
|
||||
global,
|
||||
can_gc,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl TextMetricsMethods<crate::DomTypeHolder> for TextMetrics {
|
||||
|
|
|
@ -18,6 +18,7 @@ webgl_backtrace = []
|
|||
base = { workspace = true }
|
||||
crossbeam-channel = { workspace = true }
|
||||
euclid = { workspace = true }
|
||||
fonts_traits = { workspace = true }
|
||||
glow = { workspace = true }
|
||||
ipc-channel = { workspace = true }
|
||||
kurbo = { workspace = true, features = ["serde"] }
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::str::FromStr;
|
|||
use euclid::Angle;
|
||||
use euclid::approxeq::ApproxEq;
|
||||
use euclid::default::{Point2D, Rect, Size2D, Transform2D};
|
||||
use fonts_traits::{FontDataAndIndex, FontIdentifier};
|
||||
use ipc_channel::ipc::IpcSender;
|
||||
use kurbo::{BezPath, ParamCurveNearest as _, PathEl, Point, Shape, Triangle};
|
||||
use malloc_size_of::MallocSizeOf;
|
||||
|
@ -16,8 +17,6 @@ use pixels::IpcSnapshot;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumString};
|
||||
use style::color::AbsoluteColor;
|
||||
use style::properties::style_structs::Font as FontStyleStruct;
|
||||
use style::servo_arc::Arc as ServoArc;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Path(pub BezPath);
|
||||
|
@ -437,14 +436,6 @@ pub struct LineOptions {
|
|||
pub dash_offset: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
|
||||
pub struct TextOptions {
|
||||
#[ignore_malloc_size_of = "Arc"]
|
||||
pub font: Option<ServoArc<FontStyleStruct>>,
|
||||
pub align: TextAlign,
|
||||
pub baseline: TextBaseline,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum CanvasMsg {
|
||||
|
@ -493,13 +484,9 @@ pub enum Canvas2dMsg {
|
|||
Transform2D<f64>,
|
||||
),
|
||||
FillText(
|
||||
String,
|
||||
f64,
|
||||
f64,
|
||||
Option<f64>,
|
||||
Rect<f64>,
|
||||
Vec<TextRun>,
|
||||
FillOrStrokeStyle,
|
||||
bool,
|
||||
TextOptions,
|
||||
ShadowOptions,
|
||||
CompositionOptions,
|
||||
Transform2D<f64>,
|
||||
|
@ -512,7 +499,6 @@ pub enum Canvas2dMsg {
|
|||
Transform2D<f64>,
|
||||
),
|
||||
GetImageData(Option<Rect<u32>>, IpcSender<IpcSnapshot>),
|
||||
MeasureText(String, IpcSender<TextMetrics>, TextOptions),
|
||||
PutImageData(Rect<u32>, IpcSnapshot),
|
||||
StrokeRect(
|
||||
Rect<f32>,
|
||||
|
@ -777,80 +763,46 @@ impl FromStr for CompositionOrBlending {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
MallocSizeOf,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
)]
|
||||
pub enum TextAlign {
|
||||
#[default]
|
||||
Start,
|
||||
End,
|
||||
Left,
|
||||
Right,
|
||||
Center,
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct GlyphAndPosition {
|
||||
pub id: u32,
|
||||
pub point: Point2D<f32>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
MallocSizeOf,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
)]
|
||||
pub enum TextBaseline {
|
||||
Top,
|
||||
Hanging,
|
||||
Middle,
|
||||
#[default]
|
||||
Alphabetic,
|
||||
Ideographic,
|
||||
Bottom,
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct CanvasFont {
|
||||
/// A [`FontIdentifier`] for this [`CanvasFont`], maybe either `Local` or `Web`.
|
||||
pub identifier: FontIdentifier,
|
||||
/// If this font is a web font, this field contains the data for the font. If
|
||||
/// the font is a local font, it will be `None`.
|
||||
pub data: Option<FontDataAndIndex>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Default,
|
||||
Deserialize,
|
||||
Display,
|
||||
EnumString,
|
||||
MallocSizeOf,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
)]
|
||||
pub enum Direction {
|
||||
Ltr,
|
||||
Rtl,
|
||||
#[default]
|
||||
Inherit,
|
||||
impl CanvasFont {
|
||||
pub fn font_data_and_index(&self) -> Option<FontDataAndIndex> {
|
||||
self.data.clone().or_else(|| match &self.identifier {
|
||||
FontIdentifier::Local(local_font_identifier) => {
|
||||
local_font_identifier.font_data_and_index()
|
||||
},
|
||||
FontIdentifier::Web(_) => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, MallocSizeOf, Serialize)]
|
||||
pub struct TextMetrics {
|
||||
pub width: f32,
|
||||
pub actual_boundingbox_left: f32,
|
||||
pub actual_boundingbox_right: f32,
|
||||
pub actual_boundingbox_ascent: f32,
|
||||
pub actual_boundingbox_descent: f32,
|
||||
pub font_boundingbox_ascent: f32,
|
||||
pub font_boundingbox_descent: f32,
|
||||
pub em_height_ascent: f32,
|
||||
pub em_height_descent: f32,
|
||||
pub hanging_baseline: f32,
|
||||
pub alphabetic_baseline: f32,
|
||||
pub ideographic_baseline: f32,
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct TextRun {
|
||||
pub font: CanvasFont,
|
||||
pub pt_size: f32,
|
||||
pub glyphs_and_positions: Vec<GlyphAndPosition>,
|
||||
pub advance: f32,
|
||||
pub bounds: Rect<f64>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TextRun {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("TextRun")
|
||||
.field("glyphs_and_positions", &self.glyphs_and_positions)
|
||||
.field("size", &self.bounds)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ pub type StylesheetWebFontLoadFinishedCallback = Arc<dyn Fn(bool) + Send + Sync
|
|||
/// A data structure to store data for fonts. Data is stored internally in an
|
||||
/// [`IpcSharedMemory`] handle, so that it can be sent without serialization
|
||||
/// across IPC channels.
|
||||
#[derive(Clone, MallocSizeOf)]
|
||||
#[derive(Clone, Deserialize, MallocSizeOf, Serialize)]
|
||||
pub struct FontData(#[conditional_malloc_size_of] pub(crate) Arc<IpcSharedMemory>);
|
||||
|
||||
impl FontData {
|
||||
|
@ -55,7 +55,7 @@ impl AsRef<[u8]> for FontData {
|
|||
///
|
||||
/// If the font data is of a TTC (TrueType collection) file, then the index of a specific font within
|
||||
/// the collection. If the font data is for is single font then the index will always be 0.
|
||||
#[derive(Clone)]
|
||||
#[derive(Deserialize, Clone, Serialize)]
|
||||
pub struct FontDataAndIndex {
|
||||
/// The raw font file data (.ttf, .otf, .ttc, etc)
|
||||
pub data: FontData,
|
||||
|
|
42
tests/wpt/meta/css/css-fonts/generic-family-keywords-003.html.ini
vendored
Normal file
42
tests/wpt/meta/css/css-fonts/generic-family-keywords-003.html.ini
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
[generic-family-keywords-003.html]
|
||||
[@font-face matching for quoted and unquoted math (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted ui-serif (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted ui-sans-serif (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted ui-monospace (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted ui-rounded (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted sans-serif (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted cursive (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted fantasy (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted monospace (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted system-ui (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted generic(fangsong) (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted generic(kai) (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted generic(khmer-mul) (drawing text in a canvas)]
|
||||
expected: FAIL
|
||||
|
||||
[@font-face matching for quoted and unquoted generic(nastaliq) (drawing text in a canvas)]
|
||||
expected: FAIL
|
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.condensed.html.ini
vendored
Normal file
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.condensed.html.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[canvas.2d.fontStretch.condensed.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.expanded.html.ini
vendored
Normal file
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.expanded.html.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[canvas.2d.fontStretch.expanded.html]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[canvas.2d.fontStretch.extra-condensed.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.extra-expanded.html.ini
vendored
Normal file
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.extra-expanded.html.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[canvas.2d.fontStretch.extra-expanded.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.semi-condensed.html.ini
vendored
Normal file
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.semi-condensed.html.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[canvas.2d.fontStretch.semi-condensed.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.semi-expanded.html.ini
vendored
Normal file
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.semi-expanded.html.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[canvas.2d.fontStretch.semi-expanded.html]
|
||||
expected: FAIL
|
|
@ -0,0 +1,2 @@
|
|||
[canvas.2d.fontStretch.ultra-condensed.html]
|
||||
expected: FAIL
|
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.ultra-expanded.html.ini
vendored
Normal file
2
tests/wpt/meta/html/canvas/element/manual/text/canvas.2d.fontStretch.ultra-expanded.html.ini
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[canvas.2d.fontStretch.ultra-expanded.html]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.align.center.html]
|
||||
[textAlign center is the center of the em squares (not the bounding box)]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.align.end.ltr.html]
|
||||
[textAlign end with ltr is the right edge]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.align.left.html]
|
||||
[textAlign left is the left of the first em square (not the bounding box)]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.align.right.html]
|
||||
[textAlign right is the right of the last em square (not the bounding box)]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.align.start.ltr.html]
|
||||
[textAlign start with ltr is the left edge]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.baseline.alphabetic.html]
|
||||
[Canvas test: 2d.text.draw.baseline.alphabetic]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.baseline.bottom.html]
|
||||
[textBaseline bottom is the bottom of the em square (not the bounding box)]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.baseline.middle.html]
|
||||
[textBaseline middle is the middle of the em square (not the bounding box)]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.baseline.top.html]
|
||||
[textBaseline top is the top of the em square (not the bounding box)]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.fill.maxWidth.bound.html]
|
||||
[fillText handles maxWidth based on line size, not bounding box size]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.fontface.html]
|
||||
[Canvas test: 2d.text.draw.fontface]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.fontface.notinpage.html]
|
||||
[@font-face fonts should work even if they are not used in the page]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.fontface.repeat.html]
|
||||
[Draw with the font immediately, then wait a bit until and draw again. (This crashes some version of WebKit.)]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.space.collapse.other.html]
|
||||
[Space characters are converted to U+0020, and are NOT collapsed]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.draw.space.collapse.space.html]
|
||||
[Space characters are converted to U+0020, and are NOT collapsed]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.measure.emHeights.html]
|
||||
[Testing emHeights]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.measure.fontBoundingBox.ahem.html]
|
||||
[Testing fontBoundingBox for font ahem]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.measure.width.basic.html]
|
||||
[The width of character is same as font used]
|
||||
expected: FAIL
|
|
@ -0,0 +1,3 @@
|
|||
[2d.gradient.interpolate.zerosize.fillText.html]
|
||||
[OffscreenCanvas test: 2d.gradient.interpolate.zerosize.fillText]
|
||||
expected: FAIL
|
|
@ -0,0 +1,3 @@
|
|||
[2d.gradient.interpolate.zerosize.fillText.worker.html]
|
||||
[2d]
|
||||
expected: FAIL
|
|
@ -2,9 +2,6 @@
|
|||
[Test that drawing sans-serif produces the same result between canvas and OffscreenCanvas]
|
||||
expected: FAIL
|
||||
|
||||
[Test that drawing serif produces the same result between canvas and OffscreenCanvas]
|
||||
expected: FAIL
|
||||
|
||||
[Test that drawing cursive produces the same result between canvas and OffscreenCanvas]
|
||||
expected: FAIL
|
||||
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.drawing.style.measure.rtl.text.html]
|
||||
[Measurement should follow canvas direction instead text direction]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.drawing.style.measure.rtl.text.worker.html]
|
||||
[Measurement should follow canvas direction instead text direction]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.drawing.style.measure.textAlign.html]
|
||||
[Measurement should be related to textAlignment]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[2d.text.drawing.style.measure.textAlign.worker.html]
|
||||
[Measurement should be related to textAlignment]
|
||||
expected: FAIL
|
3
tests/wpt/meta/html/canvas/offscreen/text/2d.text.measure.width.nullCharacter.html.ini
vendored
Normal file
3
tests/wpt/meta/html/canvas/offscreen/text/2d.text.measure.width.nullCharacter.html.ini
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[2d.text.measure.width.nullCharacter.html]
|
||||
[Null character does not take up space]
|
||||
expected: FAIL
|
Loading…
Add table
Add a link
Reference in a new issue