mirror of
https://github.com/penpot/penpot.git
synced 2026-09-13 15:38:38 +00:00
🐛 Fix letter spacing and position when flattening text to path (#11555)
* 🐛 Fix letter spacing and position when flattening text to path * ♻️ Simplify text to path conversion and add flatten tests
This commit is contained in:
parent
37dab75e1a
commit
0913545b41
1332
frontend/playwright/data/render-wasm/get-file-text-flatten.json
Normal file
1332
frontend/playwright/data/render-wasm/get-file-text-flatten.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -653,3 +653,32 @@ test("Renders background blur on text shapes", async ({ page }) => {
|
|||||||
await workspace.waitForFirstRenderWithoutUI();
|
await workspace.waitForFirstRenderWithoutUI();
|
||||||
await expect(workspace.canvas).toHaveScreenshot();
|
await expect(workspace.canvas).toHaveScreenshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Flattens texts to paths", async ({ page }) => {
|
||||||
|
const workspace = new WasmWorkspacePage(page);
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockGetFile("render-wasm/get-file-text-flatten.json");
|
||||||
|
|
||||||
|
await workspace.goToWorkspace({
|
||||||
|
id: "3b0d758a-8c9d-8013-8006-52c8337e5c72",
|
||||||
|
pageId: "3b0d758a-8c9d-8013-8006-52c8337e5c73",
|
||||||
|
});
|
||||||
|
await workspace.waitForFirstRender();
|
||||||
|
|
||||||
|
const flattenButton = workspace.page.getByRole("button", {
|
||||||
|
name: "Flatten",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const layer of ["Flat spacing", "Flat paragraph", "Flat centered"]) {
|
||||||
|
await workspace.clickLeafLayer(layer);
|
||||||
|
const renderCount = await workspace.getRenderCount();
|
||||||
|
await flattenButton.click();
|
||||||
|
await workspace.waitForNextRender(renderCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
await workspace.page.keyboard.press("Escape");
|
||||||
|
await workspace.hideUI();
|
||||||
|
|
||||||
|
await expect(workspace.canvas).toHaveScreenshot({ timeout: 10000 });
|
||||||
|
});
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@ -370,6 +370,12 @@
|
|||||||
(.-type ^ShapeProxy self)
|
(.-type ^ShapeProxy self)
|
||||||
delegate')))))
|
delegate')))))
|
||||||
|
|
||||||
|
(def ^:private base-fields
|
||||||
|
"Base fields of the `Shape` record this proxy stands in for: `cr/defrecord`
|
||||||
|
nils them on dissoc instead of removing them, and the schema requires them."
|
||||||
|
#{:name :x :y :width :height :rotation :selrect :points
|
||||||
|
:transform :transform-inverse :parent-id :frame-id :flip-x :flip-y})
|
||||||
|
|
||||||
(defn- impl-dissoc
|
(defn- impl-dissoc
|
||||||
[self k]
|
[self k]
|
||||||
(when shape/*shape-changes*
|
(when shape/*shape-changes*
|
||||||
@ -385,7 +391,9 @@
|
|||||||
nil
|
nil
|
||||||
(.-delegate ^ShapeProxy self))
|
(.-delegate ^ShapeProxy self))
|
||||||
(let [delegate (.-delegate ^ShapeProxy self)
|
(let [delegate (.-delegate ^ShapeProxy self)
|
||||||
delegate' (dissoc delegate k)]
|
delegate' (if (contains? base-fields k)
|
||||||
|
(assoc delegate k nil)
|
||||||
|
(dissoc delegate k))]
|
||||||
(if (identical? delegate delegate')
|
(if (identical? delegate delegate')
|
||||||
self
|
self
|
||||||
(ShapeProxy. (.-id ^ShapeProxy self)
|
(ShapeProxy. (.-id ^ShapeProxy self)
|
||||||
|
|||||||
@ -132,10 +132,6 @@ impl FontStore {
|
|||||||
&self.fallback_fonts
|
&self.fallback_fonts
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_emoji_font(&self, _size: f32) -> Option<Font> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_source_url(&mut self, alias: &str, url: String) {
|
pub fn set_source_url(&mut self, alias: &str, url: String) {
|
||||||
if !url.is_empty() {
|
if !url.is_empty() {
|
||||||
self.source_urls.insert(alias.to_string(), url);
|
self.source_urls.insert(alias.to_string(), url);
|
||||||
|
|||||||
@ -3,9 +3,9 @@ use crate::{
|
|||||||
error::Result,
|
error::Result,
|
||||||
math::Rect,
|
math::Rect,
|
||||||
shapes::{
|
shapes::{
|
||||||
add_text_with_tabs, calculate_text_layout_data, set_paint_fill, Paragraph as TextParagraph,
|
add_text_with_tabs, calculate_text_layout_data, set_paint_fill, vertical_align_offset,
|
||||||
ParagraphBuilderGroup, ParagraphLayout, Stroke, StrokeKind, TextContent,
|
Paragraph as TextParagraph, ParagraphBuilderGroup, ParagraphLayout, Stroke, StrokeKind,
|
||||||
TextDecorationSegment, VerticalAlign,
|
TextContent, TextDecorationSegment,
|
||||||
},
|
},
|
||||||
utils::{get_fallback_fonts, get_font_collection},
|
utils::{get_fallback_fonts, get_font_collection},
|
||||||
};
|
};
|
||||||
@ -379,11 +379,8 @@ fn paint_from_cached_layout(canvas: &Canvas, shape: &Shape, text_content: &TextC
|
|||||||
.filter_map(|group| group.first())
|
.filter_map(|group| group.first())
|
||||||
.map(|p| p.height())
|
.map(|p| p.height())
|
||||||
.sum();
|
.sum();
|
||||||
let vertical_offset = match shape.vertical_align() {
|
let vertical_offset =
|
||||||
VerticalAlign::Center => (selrect.height() - total_text_height) / 2.0,
|
vertical_align_offset(selrect.height(), total_text_height, shape.vertical_align());
|
||||||
VerticalAlign::Bottom => selrect.height() - total_text_height,
|
|
||||||
_ => 0.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut y_accum = base_y + vertical_offset;
|
let mut y_accum = base_y + vertical_offset;
|
||||||
for (index, group) in paragraphs.iter().enumerate() {
|
for (index, group) in paragraphs.iter().enumerate() {
|
||||||
@ -1235,32 +1232,6 @@ fn calculate_decoration_metrics(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// How to use it?
|
|
||||||
// Type::Text(text_content) => {
|
|
||||||
// self.surfaces
|
|
||||||
// .apply_mut(&[SurfaceId::Fills, SurfaceId::Strokes], |s| {
|
|
||||||
// s.canvas().concat(&matrix);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// let text_content = text_content.new_bounds(shape.selrect());
|
|
||||||
// let paths = text_content.get_paths(antialias);
|
|
||||||
|
|
||||||
// shadows::render_text_shadows(self, &shape, &paths, antialias);
|
|
||||||
// text::render(self, &paths, None, None);
|
|
||||||
|
|
||||||
// for stroke in shape.visible_strokes().rev() {
|
|
||||||
// shadows::render_text_path_stroke_shadows(
|
|
||||||
// self, &shape, &paths, stroke, antialias,
|
|
||||||
// );
|
|
||||||
// strokes::render_text_paths(self, &shape, stroke, &paths, None, None, antialias);
|
|
||||||
// shadows::render_text_path_stroke_inner_shadows(
|
|
||||||
// self, &shape, &paths, stroke, antialias,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// shadows::render_text_inner_shadows(self, &shape, &paths, antialias);
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::render::options::RenderOptions;
|
use crate::render::options::RenderOptions;
|
||||||
use crate::shapes::{Shape, TextContent, Type, VerticalAlign};
|
use crate::shapes::{vertical_align_offset, Shape, TextContent, Type};
|
||||||
use crate::state::{TextEditorState, TextSelection};
|
use crate::state::{TextEditorState, TextSelection};
|
||||||
use crate::view::Viewbox;
|
use crate::view::Viewbox;
|
||||||
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
||||||
@ -112,16 +112,16 @@ fn render_selection(
|
|||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vertical_align_offset(
|
fn paragraphs_vertical_offset(
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
layout_paragraphs: &[&skia_safe::textlayout::Paragraph],
|
layout_paragraphs: &[&skia_safe::textlayout::Paragraph],
|
||||||
) -> f32 {
|
) -> f32 {
|
||||||
let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum();
|
let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum();
|
||||||
match shape.vertical_align() {
|
vertical_align_offset(
|
||||||
VerticalAlign::Center => (shape.selrect().height() - total_height) / 2.0,
|
shape.selrect().height(),
|
||||||
VerticalAlign::Bottom => shape.selrect().height() - total_height,
|
total_height,
|
||||||
_ => 0.0,
|
shape.vertical_align(),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_cursor_rect(
|
fn calculate_cursor_rect(
|
||||||
@ -141,7 +141,7 @@ fn calculate_cursor_rect(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs);
|
||||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||||
if idx == cursor.paragraph {
|
if idx == cursor.paragraph {
|
||||||
let char_pos = cursor.offset;
|
let char_pos = cursor.offset;
|
||||||
@ -236,7 +236,7 @@ fn calculate_selection_rects(
|
|||||||
let paragraphs = text_content.paragraphs();
|
let paragraphs = text_content.paragraphs();
|
||||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
||||||
|
|
||||||
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
let mut y_offset = paragraphs_vertical_offset(shape, &layout_paragraphs);
|
||||||
|
|
||||||
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||||
let para_height = laid_out_para.height();
|
let para_height = laid_out_para.height();
|
||||||
|
|||||||
@ -264,9 +264,9 @@ impl ToPath for Shape {
|
|||||||
Type::SVGRaw(_) => Path::default(),
|
Type::SVGRaw(_) => Path::default(),
|
||||||
|
|
||||||
Type::Text(ref text) => {
|
Type::Text(ref text) => {
|
||||||
let text_paths = TextPaths::new(text.clone());
|
let text_paths = TextPaths::new(text.new_bounds(self.selrect()));
|
||||||
let mut result = Path::default();
|
let mut result = Path::default();
|
||||||
for (path, _) in text_paths.get_paths(true) {
|
for path in text_paths.get_paths(self.vertical_align()) {
|
||||||
result = join_paths(result, Path::from_skia_path(path));
|
result = join_paths(result, Path::from_skia_path(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -332,7 +332,7 @@ impl TextDecorationSegment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vertical_align_offset(container_h: f32, content_h: f32, valign: VerticalAlign) -> f32 {
|
pub fn vertical_align_offset(container_h: f32, content_h: f32, valign: VerticalAlign) -> f32 {
|
||||||
match valign {
|
match valign {
|
||||||
VerticalAlign::Center => (container_h - content_h) / 2.0,
|
VerticalAlign::Center => (container_h - content_h) / 2.0,
|
||||||
VerticalAlign::Bottom => container_h - content_h,
|
VerticalAlign::Bottom => container_h - content_h,
|
||||||
@ -1859,11 +1859,8 @@ pub fn calculate_text_layout_data(
|
|||||||
|
|
||||||
// 2. Position each built paragraph using the heights from step 1.
|
// 2. Position each built paragraph using the heights from step 1.
|
||||||
let total_text_height: f32 = paragraph_heights.iter().sum();
|
let total_text_height: f32 = paragraph_heights.iter().sum();
|
||||||
let vertical_offset = match shape.vertical_align() {
|
let vertical_offset =
|
||||||
VerticalAlign::Center => (selrect_height - total_text_height) / 2.0,
|
vertical_align_offset(selrect_height, total_text_height, shape.vertical_align());
|
||||||
VerticalAlign::Bottom => selrect_height - total_text_height,
|
|
||||||
_ => 0.0,
|
|
||||||
};
|
|
||||||
let mut paragraph_layouts: Vec<ParagraphLayout> = Vec::new();
|
let mut paragraph_layouts: Vec<ParagraphLayout> = Vec::new();
|
||||||
let mut y_accum = base_y + vertical_offset;
|
let mut y_accum = base_y + vertical_offset;
|
||||||
for (i, group_paragraphs) in built_groups.into_iter().enumerate() {
|
for (i, group_paragraphs) in built_groups.into_iter().enumerate() {
|
||||||
@ -1971,6 +1968,39 @@ pub fn calculate_position_data(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vertical_align_top_keeps_the_content_at_the_origin() {
|
||||||
|
assert_eq!(vertical_align_offset(200.0, 60.0, VerticalAlign::Top), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vertical_align_center_takes_half_the_slack() {
|
||||||
|
assert_eq!(
|
||||||
|
vertical_align_offset(200.0, 60.0, VerticalAlign::Center),
|
||||||
|
70.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vertical_align_bottom_takes_all_the_slack() {
|
||||||
|
assert_eq!(
|
||||||
|
vertical_align_offset(200.0, 60.0, VerticalAlign::Bottom),
|
||||||
|
140.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vertical_align_offset_is_negative_when_content_overflows() {
|
||||||
|
assert_eq!(
|
||||||
|
vertical_align_offset(60.0, 200.0, VerticalAlign::Center),
|
||||||
|
-70.0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vertical_align_offset(60.0, 200.0, VerticalAlign::Bottom),
|
||||||
|
-140.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn capitalize_basic_words() {
|
fn capitalize_basic_words() {
|
||||||
assert_eq!(capitalize_words("hello world"), "Hello World");
|
assert_eq!(capitalize_words("hello world"), "Hello World");
|
||||||
|
|||||||
@ -1,190 +1,81 @@
|
|||||||
use crate::get_resources;
|
use crate::render::text::decoration_segments;
|
||||||
use crate::shapes::text::TextContent;
|
use crate::shapes::text::{vertical_align_offset, Paragraph, TextContent};
|
||||||
|
use crate::shapes::VerticalAlign;
|
||||||
use skia_safe::{
|
use skia_safe::{
|
||||||
self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob,
|
self as skia,
|
||||||
|
textlayout::{paragraph::VisitorInfo, Paragraph as SkiaParagraph},
|
||||||
|
Point,
|
||||||
};
|
};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
pub struct TextPaths(TextContent);
|
pub struct TextPaths(TextContent);
|
||||||
|
|
||||||
// Note: This class is not being currently used.
|
|
||||||
// It's an example of how to convert texts to paths
|
|
||||||
#[allow(dead_code)]
|
|
||||||
impl TextPaths {
|
impl TextPaths {
|
||||||
pub fn new(text_content: TextContent) -> Self {
|
pub fn new(text_content: TextContent) -> Self {
|
||||||
Self(text_content)
|
Self(text_content)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> {
|
pub fn get_paths(&self, vertical_align: VerticalAlign) -> Vec<skia::Path> {
|
||||||
let mut paths = Vec::new();
|
let layout_width = self.0.get_width(self.bounds.width());
|
||||||
let mut offset_y = self.bounds.y();
|
|
||||||
let mut paragraph_builders = self.0.paragraph_builder_group_from_text(None);
|
let mut paragraph_builders = self.0.paragraph_builder_group_from_text(None);
|
||||||
|
let mut paragraphs: Vec<SkiaParagraph> = paragraph_builders
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|group| group.first_mut())
|
||||||
|
.map(|paragraph_builder| {
|
||||||
|
let mut paragraph = paragraph_builder.build();
|
||||||
|
paragraph.layout(layout_width);
|
||||||
|
paragraph
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
for paragraphs in paragraph_builders.iter_mut() {
|
let total_height: f32 = paragraphs.iter().map(|p| p.height()).sum();
|
||||||
for paragraph_builder in paragraphs.iter_mut() {
|
let mut offset_y = self.bounds.y()
|
||||||
// 1. Get paragraph and set the width layout
|
+ vertical_align_offset(self.bounds.height(), total_height, vertical_align);
|
||||||
let mut skia_paragraph = paragraph_builder.build();
|
|
||||||
let text = paragraph_builder.get_text();
|
|
||||||
let paragraph_width = self.bounds.width();
|
|
||||||
skia_paragraph.layout(paragraph_width);
|
|
||||||
|
|
||||||
let mut line_offset_y = offset_y;
|
let mut paths = Vec::new();
|
||||||
|
for (paragraph, text_paragraph) in paragraphs.iter_mut().zip(self.0.paragraphs()) {
|
||||||
// 2. Iterate through each line in the paragraph
|
let origin = Point::new(self.bounds.x(), offset_y);
|
||||||
for line_metrics in skia_paragraph.get_line_metrics() {
|
Self::collect_paragraph_paths(paragraph, text_paragraph, origin, &mut paths);
|
||||||
let line_baseline = line_metrics.baseline as f32;
|
offset_y += paragraph.height();
|
||||||
let start = line_metrics.start_index;
|
|
||||||
let end = line_metrics.end_index;
|
|
||||||
|
|
||||||
// 3. Get styles present in line for each text span
|
|
||||||
let style_metrics = line_metrics.get_style_metrics(start..end);
|
|
||||||
|
|
||||||
let mut offset_x = 0.0;
|
|
||||||
|
|
||||||
for (i, (start_index, style_metric)) in style_metrics.iter().enumerate() {
|
|
||||||
let end_index = style_metrics.get(i + 1).map_or(end, |next| next.0);
|
|
||||||
|
|
||||||
let start_byte = text
|
|
||||||
.char_indices()
|
|
||||||
.nth(*start_index)
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
.unwrap_or(0);
|
|
||||||
let end_byte = text
|
|
||||||
.char_indices()
|
|
||||||
.nth(end_index)
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
.unwrap_or(text.len());
|
|
||||||
|
|
||||||
let span_text = &text[start_byte..end_byte];
|
|
||||||
|
|
||||||
let font = skia_paragraph.get_font_at(*start_index);
|
|
||||||
|
|
||||||
let blob_offset_x = self.bounds.x() + line_metrics.left as f32 + offset_x;
|
|
||||||
let blob_offset_y = line_offset_y;
|
|
||||||
|
|
||||||
// 4. Get the path for each text span
|
|
||||||
if let Some((text_path, paint)) = self.generate_text_path(
|
|
||||||
span_text,
|
|
||||||
&font,
|
|
||||||
blob_offset_x,
|
|
||||||
blob_offset_y,
|
|
||||||
style_metric,
|
|
||||||
antialias,
|
|
||||||
) {
|
|
||||||
let text_width = font.measure_text(span_text, None).0;
|
|
||||||
offset_x += text_width;
|
|
||||||
paths.push((text_path, paint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line_offset_y = offset_y + line_baseline;
|
|
||||||
}
|
|
||||||
offset_y += skia_paragraph.height();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_text_path(
|
fn collect_paragraph_paths(
|
||||||
&self,
|
paragraph: &mut SkiaParagraph,
|
||||||
span_text: &str,
|
text_paragraph: &Paragraph,
|
||||||
font: &skia::Font,
|
origin: Point,
|
||||||
blob_offset_x: f32,
|
paths: &mut Vec<skia::Path>,
|
||||||
blob_offset_y: f32,
|
) {
|
||||||
style_metric: &skia::textlayout::StyleMetrics,
|
for deco in decoration_segments(paragraph, text_paragraph, origin.x, origin.y) {
|
||||||
antialias: bool,
|
let mut builder = skia::PathBuilder::new();
|
||||||
) -> Option<(skia::Path, skia::Paint)> {
|
builder.add_rect(deco.rect(), None, None);
|
||||||
// Convert text to path, including text decoration
|
paths.push(builder.detach());
|
||||||
// TextBlob might be empty and, in this case, we return None
|
}
|
||||||
// This is used to avoid rendering empty paths, but we can
|
|
||||||
// revisit this logic later
|
|
||||||
if let Some((text_blob_path, text_blob_bounds)) =
|
|
||||||
Self::get_text_blob_path(span_text, font, blob_offset_x, blob_offset_y)
|
|
||||||
{
|
|
||||||
let text_width = font.measure_text(span_text, None).0;
|
|
||||||
|
|
||||||
let decoration = style_metric.text_style.decoration();
|
paragraph.visit(|_: usize, info: Option<&VisitorInfo>| {
|
||||||
let font_metrics = style_metric.font_metrics;
|
let Some(info) = info else {
|
||||||
|
return;
|
||||||
let blob_left = blob_offset_x;
|
|
||||||
let blob_top = blob_offset_y;
|
|
||||||
let blob_height = text_blob_bounds.height();
|
|
||||||
|
|
||||||
let text_path = {
|
|
||||||
let mut pb = skia::PathBuilder::new_path(&text_blob_path);
|
|
||||||
if let Some(decoration_rect) = self.calculate_text_decoration_rect(
|
|
||||||
decoration.ty,
|
|
||||||
font_metrics,
|
|
||||||
blob_left,
|
|
||||||
blob_top,
|
|
||||||
text_width,
|
|
||||||
blob_height,
|
|
||||||
) {
|
|
||||||
pb.add_rect(decoration_rect, None, None);
|
|
||||||
}
|
|
||||||
pb.detach()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut paint = style_metric.text_style.foreground();
|
let font = info.font();
|
||||||
paint.set_anti_alias(antialias);
|
let run_origin = origin + info.origin();
|
||||||
|
let mut builder = skia::PathBuilder::new();
|
||||||
|
let mut has_glyphs = false;
|
||||||
|
|
||||||
return Some((text_path, paint));
|
for (glyph, position) in info.glyphs().iter().zip(info.positions().iter()) {
|
||||||
}
|
let Some(glyph_path) = font.get_path(*glyph) else {
|
||||||
None
|
continue;
|
||||||
}
|
};
|
||||||
|
builder.add_path(&glyph_path.with_offset(run_origin + *position));
|
||||||
fn calculate_text_decoration_rect(
|
has_glyphs = true;
|
||||||
&self,
|
|
||||||
decoration: skia::textlayout::TextDecoration,
|
|
||||||
font_metrics: FontMetrics,
|
|
||||||
blob_left: f32,
|
|
||||||
blob_offset_y: f32,
|
|
||||||
text_width: f32,
|
|
||||||
blob_height: f32,
|
|
||||||
) -> Option<Rect> {
|
|
||||||
match decoration {
|
|
||||||
skia::textlayout::TextDecoration::LINE_THROUGH => {
|
|
||||||
let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0);
|
|
||||||
let underline_position = blob_height / 2.0;
|
|
||||||
Some(Rect::new(
|
|
||||||
blob_left,
|
|
||||||
blob_offset_y + underline_position - underline_thickness / 2.0,
|
|
||||||
blob_left + text_width,
|
|
||||||
blob_offset_y + underline_position + underline_thickness / 2.0,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
skia::textlayout::TextDecoration::UNDERLINE => {
|
|
||||||
let underline_thickness = font_metrics.underline_thickness().unwrap_or(0.0);
|
if has_glyphs {
|
||||||
let underline_position = blob_height - underline_thickness;
|
paths.push(builder.detach());
|
||||||
Some(Rect::new(
|
|
||||||
blob_left,
|
|
||||||
blob_offset_y + underline_position - underline_thickness / 2.0,
|
|
||||||
blob_left + text_width,
|
|
||||||
blob_offset_y + underline_position + underline_thickness / 2.0,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
_ => None,
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_text_blob_path(
|
|
||||||
span_text: &str,
|
|
||||||
font: &skia::Font,
|
|
||||||
blob_offset_x: f32,
|
|
||||||
blob_offset_y: f32,
|
|
||||||
) -> Option<(skia::Path, skia::Rect)> {
|
|
||||||
let utf16_text = span_text.encode_utf16().collect::<Vec<u16>>();
|
|
||||||
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
|
|
||||||
let emoji_font = get_resources().fonts.get_emoji_font(font.size());
|
|
||||||
let use_font = emoji_font.as_ref().unwrap_or(font);
|
|
||||||
|
|
||||||
if let Some(mut text_blob) = TextBlob::from_text(text, use_font) {
|
|
||||||
let path = SkiaParagraph::get_path(&mut text_blob);
|
|
||||||
let d = Point::new(blob_offset_x, blob_offset_y);
|
|
||||||
let offset_path = path.with_offset(d);
|
|
||||||
let bounds = text_blob.bounds();
|
|
||||||
return Some((offset_path, *bounds));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user