mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
198 lines
7.4 KiB
Rust
198 lines
7.4 KiB
Rust
use crate::shapes::text::TextContent;
|
|
use skia_safe::{
|
|
self as skia, textlayout::Paragraph as SkiaParagraph, FontMetrics, Point, Rect, TextBlob,
|
|
};
|
|
use std::ops::Deref;
|
|
|
|
use crate::{with_state_mut, STATE};
|
|
|
|
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 {
|
|
pub fn new(text_content: TextContent) -> Self {
|
|
Self(text_content)
|
|
}
|
|
|
|
pub fn get_paths(&self, antialias: bool) -> Vec<(skia::Path, skia::Paint)> {
|
|
let mut paths = Vec::new();
|
|
let mut offset_y = self.bounds.y();
|
|
let mut paragraph_builders = self.0.paragraph_builder_group_from_text(None);
|
|
|
|
for paragraphs in paragraph_builders.iter_mut() {
|
|
for paragraph_builder in paragraphs.iter_mut() {
|
|
// 1. Get paragraph and set the width layout
|
|
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;
|
|
|
|
// 2. Iterate through each line in the paragraph
|
|
for line_metrics in skia_paragraph.get_line_metrics() {
|
|
let line_baseline = line_metrics.baseline as f32;
|
|
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
|
|
}
|
|
|
|
fn generate_text_path(
|
|
&self,
|
|
span_text: &str,
|
|
font: &skia::Font,
|
|
blob_offset_x: f32,
|
|
blob_offset_y: f32,
|
|
style_metric: &skia::textlayout::StyleMetrics,
|
|
antialias: bool,
|
|
) -> Option<(skia::Path, skia::Paint)> {
|
|
// Convert text to path, including text decoration
|
|
// 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 mut text_path = text_blob_path.clone();
|
|
let text_width = font.measure_text(span_text, None).0;
|
|
|
|
let decoration = style_metric.text_style.decoration();
|
|
let font_metrics = style_metric.font_metrics;
|
|
|
|
let blob_left = blob_offset_x;
|
|
let blob_top = blob_offset_y;
|
|
let blob_height = text_blob_bounds.height();
|
|
|
|
if let Some(decoration_rect) = self.calculate_text_decoration_rect(
|
|
decoration.ty,
|
|
font_metrics,
|
|
blob_left,
|
|
blob_top,
|
|
text_width,
|
|
blob_height,
|
|
) {
|
|
text_path.add_rect(decoration_rect, None);
|
|
}
|
|
|
|
let mut paint = style_metric.text_style.foreground();
|
|
paint.set_anti_alias(antialias);
|
|
|
|
return Some((text_path, paint));
|
|
}
|
|
None
|
|
}
|
|
|
|
fn calculate_text_decoration_rect(
|
|
&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);
|
|
let underline_position = blob_height - underline_thickness;
|
|
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)> {
|
|
with_state_mut!(state, {
|
|
let utf16_text = span_text.encode_utf16().collect::<Vec<u16>>();
|
|
let text = unsafe { skia_safe::as_utf16_unchecked(&utf16_text) };
|
|
let emoji_font = state.render_state.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
|
|
}
|
|
}
|
|
|
|
impl Deref for TextPaths {
|
|
type Target = TextContent;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|