mirror of
https://github.com/servo/servo.git
synced 2025-09-23 05:10:09 +01:00
canvas: Implement strokeText
(#39183)
Mostly it's just reusing/copy&edit fillText stuff. Testing: Existing WPT tests Fixes: #29973 Try run: https://github.com/sagudev/servo/actions/runs/17511337550 --------- Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
This commit is contained in:
parent
bd3231847e
commit
643ac08cf0
28 changed files with 300 additions and 64 deletions
|
@ -78,6 +78,14 @@ pub(crate) trait GenericDrawTarget {
|
|||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
);
|
||||
fn stroke_text(
|
||||
&mut self,
|
||||
text_runs: Vec<TextRun>,
|
||||
style: FillOrStrokeStyle,
|
||||
line_options: LineOptions,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
);
|
||||
fn stroke_rect(
|
||||
&mut self,
|
||||
rect: &Rect<f32>,
|
||||
|
|
|
@ -119,6 +119,33 @@ impl<DrawTarget: GenericDrawTarget> CanvasData<DrawTarget> {
|
|||
);
|
||||
}
|
||||
|
||||
pub(crate) fn stroke_text(
|
||||
&mut self,
|
||||
text_bounds: Rect<f64>,
|
||||
text_runs: Vec<TextRun>,
|
||||
fill_or_stroke_style: FillOrStrokeStyle,
|
||||
line_options: LineOptions,
|
||||
_shadow_options: ShadowOptions,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
) {
|
||||
self.maybe_bound_shape_with_pattern(
|
||||
fill_or_stroke_style,
|
||||
composition_options,
|
||||
&text_bounds,
|
||||
transform,
|
||||
|self_, style| {
|
||||
self_.drawtarget.stroke_text(
|
||||
text_runs,
|
||||
style,
|
||||
line_options,
|
||||
composition_options,
|
||||
transform,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn fill_rect(
|
||||
&mut self,
|
||||
rect: &Rect<f32>,
|
||||
|
|
|
@ -124,6 +124,25 @@ impl CanvasPaintThread {
|
|||
transform,
|
||||
);
|
||||
},
|
||||
Canvas2dMsg::StrokeText(
|
||||
text_bounds,
|
||||
text_runs,
|
||||
fill_or_stroke_style,
|
||||
line_options,
|
||||
shadow_options,
|
||||
composition_options,
|
||||
transform,
|
||||
) => {
|
||||
self.canvas(canvas_id).stroke_text(
|
||||
text_bounds,
|
||||
text_runs,
|
||||
fill_or_stroke_style,
|
||||
line_options,
|
||||
shadow_options,
|
||||
composition_options,
|
||||
transform,
|
||||
);
|
||||
},
|
||||
Canvas2dMsg::FillRect(rect, style, shadow_options, composition_options, transform) => {
|
||||
self.canvas(canvas_id).fill_rect(
|
||||
&rect,
|
||||
|
@ -311,6 +330,40 @@ impl Canvas {
|
|||
}
|
||||
}
|
||||
|
||||
fn stroke_text(
|
||||
&mut self,
|
||||
text_bounds: Rect<f64>,
|
||||
text_runs: Vec<TextRun>,
|
||||
fill_or_stroke_style: FillOrStrokeStyle,
|
||||
line_options: LineOptions,
|
||||
shadow_options: ShadowOptions,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
) {
|
||||
match self {
|
||||
#[cfg(feature = "vello")]
|
||||
Canvas::Vello(canvas_data) => canvas_data.stroke_text(
|
||||
text_bounds,
|
||||
text_runs,
|
||||
fill_or_stroke_style,
|
||||
line_options,
|
||||
shadow_options,
|
||||
composition_options,
|
||||
transform,
|
||||
),
|
||||
#[cfg(feature = "vello_cpu")]
|
||||
Canvas::VelloCPU(canvas_data) => canvas_data.stroke_text(
|
||||
text_bounds,
|
||||
text_runs,
|
||||
fill_or_stroke_style,
|
||||
line_options,
|
||||
shadow_options,
|
||||
composition_options,
|
||||
transform,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_text(
|
||||
&mut self,
|
||||
text_bounds: Rect<f64>,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#![deny(unsafe_code)]
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
mod backend;
|
||||
|
||||
|
|
|
@ -494,6 +494,57 @@ impl GenericDrawTarget for VelloDrawTarget {
|
|||
})
|
||||
}
|
||||
|
||||
fn stroke_text(
|
||||
&mut self,
|
||||
text_runs: Vec<TextRun>,
|
||||
style: FillOrStrokeStyle,
|
||||
line_options: LineOptions,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
) {
|
||||
self.ensure_drawing();
|
||||
let pattern = convert_to_brush(style, composition_options);
|
||||
let transform = transform.cast().into();
|
||||
let line_options: kurbo::Stroke = line_options.convert();
|
||||
self.with_composition(composition_options.composition_operation, |self_| {
|
||||
for text_run in text_runs.iter() {
|
||||
SHARED_FONT_CACHE.with(|font_cache| {
|
||||
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_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 {
|
||||
return;
|
||||
};
|
||||
|
||||
self_
|
||||
.scene
|
||||
.draw_glyphs(font)
|
||||
.transform(transform)
|
||||
.brush(&pattern)
|
||||
.font_size(text_run.pt_size)
|
||||
.draw(
|
||||
&line_options,
|
||||
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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stroke_rect(
|
||||
&mut self,
|
||||
rect: &Rect<f32>,
|
||||
|
|
|
@ -384,6 +384,50 @@ impl GenericDrawTarget for VelloCPUDrawTarget {
|
|||
})
|
||||
}
|
||||
|
||||
fn stroke_text(
|
||||
&mut self,
|
||||
text_runs: Vec<TextRun>,
|
||||
style: FillOrStrokeStyle,
|
||||
line_options: LineOptions,
|
||||
composition_options: CompositionOptions,
|
||||
transform: Transform2D<f64>,
|
||||
) {
|
||||
self.ensure_drawing();
|
||||
self.ctx.set_paint(paint(style, composition_options.alpha));
|
||||
self.ctx.set_stroke(line_options.convert());
|
||||
self.ctx.set_transform(transform.cast().into());
|
||||
self.with_composition(composition_options.composition_operation, |self_| {
|
||||
for text_run in text_runs.iter() {
|
||||
SHARED_FONT_CACHE.with(|font_cache| {
|
||||
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_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 {
|
||||
return;
|
||||
};
|
||||
self_
|
||||
.ctx
|
||||
.glyph_run(font)
|
||||
.font_size(text_run.pt_size)
|
||||
.stroke_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,
|
||||
},
|
||||
));
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stroke_rect(
|
||||
&mut self,
|
||||
rect: &Rect<f32>,
|
||||
|
|
|
@ -1382,10 +1382,11 @@ impl CanvasState {
|
|||
y: f64,
|
||||
max_width: Option<f64>,
|
||||
) {
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return;
|
||||
}
|
||||
if max_width.is_some_and(|max_width| !max_width.is_finite() || max_width <= 0.) {
|
||||
// Step 1: If any of the arguments are infinite or NaN, then return.
|
||||
if !x.is_finite() ||
|
||||
!y.is_finite() ||
|
||||
max_width.is_some_and(|max_width| !max_width.is_finite())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1396,13 +1397,68 @@ impl CanvasState {
|
|||
// the initial values for the text style.
|
||||
let size = self.font_style().font_size.computed_size().px() as f64;
|
||||
|
||||
self.fill_text_with_size(
|
||||
let Some((bounds, text_run)) = self.text_with_size(
|
||||
global_scope,
|
||||
text.str(),
|
||||
Point2D::new(x, y),
|
||||
size,
|
||||
max_width,
|
||||
);
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::FillText(
|
||||
bounds,
|
||||
text_run,
|
||||
self.state.borrow().fill_style.to_fill_or_stroke_style(),
|
||||
self.state.borrow().shadow_options(),
|
||||
self.state.borrow().composition_options(),
|
||||
self.state.borrow().transform,
|
||||
));
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroketext
|
||||
pub(super) fn stroke_text(
|
||||
&self,
|
||||
global_scope: &GlobalScope,
|
||||
canvas: Option<&HTMLCanvasElement>,
|
||||
text: DOMString,
|
||||
x: f64,
|
||||
y: f64,
|
||||
max_width: Option<f64>,
|
||||
) {
|
||||
// Step 1: If any of the arguments are infinite or NaN, then return.
|
||||
if !x.is_finite() ||
|
||||
!y.is_finite() ||
|
||||
max_width.is_some_and(|max_width| !max_width.is_finite())
|
||||
{
|
||||
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 Some((bounds, text_run)) = self.text_with_size(
|
||||
global_scope,
|
||||
text.str(),
|
||||
Point2D::new(x, y),
|
||||
size,
|
||||
max_width,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::StrokeText(
|
||||
bounds,
|
||||
text_run,
|
||||
self.state.borrow().stroke_style.to_fill_or_stroke_style(),
|
||||
self.state.borrow().line_options(),
|
||||
self.state.borrow().shadow_options(),
|
||||
self.state.borrow().composition_options(),
|
||||
self.state.borrow().transform,
|
||||
));
|
||||
}
|
||||
|
||||
/// <https://html.spec.whatwg.org/multipage/#text-preparation-algorithm>
|
||||
|
@ -2189,19 +2245,24 @@ impl CanvasState {
|
|||
.map_err(|_| Error::IndexSize)
|
||||
}
|
||||
|
||||
fn fill_text_with_size(
|
||||
fn text_with_size(
|
||||
&self,
|
||||
global_scope: &GlobalScope,
|
||||
text: &str,
|
||||
origin: Point2D<f64>,
|
||||
size: f64,
|
||||
max_width: Option<f64>,
|
||||
) {
|
||||
) -> Option<(Rect<f64>, Vec<TextRun>)> {
|
||||
let Some(font_context) = global_scope.font_context() else {
|
||||
warn!("Tried to paint to a canvas of GlobalScope without a FontContext.");
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
|
||||
// Step 1: If maxWidth was provided but is less than or equal to zero or equal to NaN, then return an empty array.
|
||||
if max_width.is_some_and(|max_width| max_width.is_nan() || max_width <= 0.) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// > Step 2: Replace all ASCII whitespace in text with U+0020 SPACE characters.
|
||||
let text = replace_ascii_whitespace(text);
|
||||
|
||||
|
@ -2212,7 +2273,7 @@ impl CanvasState {
|
|||
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;
|
||||
return None;
|
||||
};
|
||||
|
||||
let runs = self.build_unshaped_text_runs(font_context, &text, &mut font_group);
|
||||
|
@ -2242,8 +2303,7 @@ impl CanvasState {
|
|||
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;
|
||||
return self.text_with_size(global_scope, &text, origin, new_size, Some(max_width));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2265,16 +2325,12 @@ impl CanvasState {
|
|||
.union(&text_run.bounds);
|
||||
}
|
||||
|
||||
self.send_canvas_2d_msg(Canvas2dMsg::FillText(
|
||||
Some((
|
||||
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>(
|
||||
|
|
|
@ -350,6 +350,19 @@ impl CanvasRenderingContext2DMethods<crate::DomTypeHolder> for CanvasRenderingCo
|
|||
self.mark_as_dirty();
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroketext
|
||||
fn StrokeText(&self, text: DOMString, x: f64, y: f64, max_width: Option<f64>) {
|
||||
self.canvas_state.stroke_text(
|
||||
&self.global(),
|
||||
self.canvas.canvas().as_deref(),
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
max_width,
|
||||
);
|
||||
self.mark_as_dirty();
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#textmetrics
|
||||
fn MeasureText(&self, text: DOMString, can_gc: CanGc) -> DomRoot<TextMetrics> {
|
||||
self.canvas_state.measure_text(
|
||||
|
|
|
@ -276,6 +276,11 @@ impl OffscreenCanvasRenderingContext2DMethods<crate::DomTypeHolder>
|
|||
self.context.FillText(text, x, y, max_width)
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-context-2d-stroketext
|
||||
fn StrokeText(&self, text: DOMString, x: f64, y: f64, max_width: Option<f64>) {
|
||||
self.context.StrokeText(text, x, y, max_width)
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#textmetrics
|
||||
fn MeasureText(&self, text: DOMString, can_gc: CanGc) -> DomRoot<TextMetrics> {
|
||||
self.context.MeasureText(text, can_gc)
|
||||
|
|
|
@ -149,8 +149,9 @@ interface mixin CanvasText {
|
|||
[Pref="dom_canvas_text_enabled"]
|
||||
undefined fillText(DOMString text, unrestricted double x, unrestricted double y,
|
||||
optional unrestricted double maxWidth);
|
||||
//void strokeText(DOMString text, unrestricted double x, unrestricted double y,
|
||||
// optional unrestricted double maxWidth);
|
||||
[Pref="dom_canvas_text_enabled"]
|
||||
undefined strokeText(DOMString text, unrestricted double x, unrestricted double y,
|
||||
optional unrestricted double maxWidth);
|
||||
[Pref="dom_canvas_text_enabled"]
|
||||
TextMetrics measureText(DOMString text);
|
||||
};
|
||||
|
|
|
@ -492,6 +492,15 @@ pub enum Canvas2dMsg {
|
|||
CompositionOptions,
|
||||
Transform2D<f64>,
|
||||
),
|
||||
StrokeText(
|
||||
Rect<f64>,
|
||||
Vec<TextRun>,
|
||||
FillOrStrokeStyle,
|
||||
LineOptions,
|
||||
ShadowOptions,
|
||||
CompositionOptions,
|
||||
Transform2D<f64>,
|
||||
),
|
||||
FillRect(
|
||||
Rect<f32>,
|
||||
FillOrStrokeStyle,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue