mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Restructure gcore/text module and fix memory leak (#3221)
* Restructure gcore/text module and fix memory leak * Remove unused import * Fix default font fallback causing wrong caching and rename to TextContext * Upgrade demo art
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
mod font_cache;
|
||||
mod path_builder;
|
||||
mod text_context;
|
||||
mod to_path;
|
||||
|
||||
use dyn_any::DynAny;
|
||||
pub use font_cache::*;
|
||||
pub use text_context::TextContext;
|
||||
pub use to_path::*;
|
||||
|
||||
/// Alignment of lines of type within a text block.
|
||||
@@ -29,3 +32,28 @@ impl From<TextAlign> for parley::Alignment {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TypesettingConfig {
|
||||
pub font_size: f64,
|
||||
pub line_height_ratio: f64,
|
||||
pub character_spacing: f64,
|
||||
pub max_width: Option<f64>,
|
||||
pub max_height: Option<f64>,
|
||||
pub tilt: f64,
|
||||
pub align: TextAlign,
|
||||
}
|
||||
|
||||
impl Default for TypesettingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: 24.,
|
||||
line_height_ratio: 1.2,
|
||||
character_spacing: 0.,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
tilt: 0.,
|
||||
align: TextAlign::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use dyn_any::DynAny;
|
||||
use parley::fontique::Blob;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A font type (storing font family and font style and an optional preview URL)
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq, DynAny, specta::Type)]
|
||||
@@ -50,8 +52,13 @@ impl FontCache {
|
||||
}
|
||||
|
||||
/// Try to get the bytes for a font
|
||||
pub fn get(&self, font: &Font) -> Option<&Vec<u8>> {
|
||||
self.resolve_font(font).and_then(|font| self.font_file_data.get(font))
|
||||
pub fn get<'a>(&'a self, font: &'a Font) -> Option<(&'a Vec<u8>, &'a Font)> {
|
||||
self.resolve_font(font).and_then(|font| self.font_file_data.get(font).map(|data| (data, font)))
|
||||
}
|
||||
|
||||
/// Get font data as a Blob for use with parley/skrifa
|
||||
pub fn get_blob<'a>(&'a self, font: &'a Font) -> Option<(Blob<u8>, &'a Font)> {
|
||||
self.get(font).map(|(data, font)| (Blob::new(Arc::new(data.clone())), font))
|
||||
}
|
||||
|
||||
/// Check if the font is already loaded
|
||||
|
||||
158
node-graph/gcore/src/text/path_builder.rs
Normal file
158
node-graph/gcore/src/text/path_builder.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use crate::subpath::{ManipulatorGroup, Subpath};
|
||||
use crate::table::{Table, TableRow};
|
||||
use crate::vector::{PointId, Vector};
|
||||
use glam::{DAffine2, DVec2};
|
||||
use parley::GlyphRun;
|
||||
use skrifa::GlyphId;
|
||||
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
|
||||
use skrifa::outline::{DrawSettings, OutlinePen};
|
||||
use skrifa::raw::FontRef as ReadFontsRef;
|
||||
use skrifa::{MetadataProvider, OutlineGlyph};
|
||||
|
||||
pub struct PathBuilder {
|
||||
current_subpath: Subpath<PointId>,
|
||||
origin: DVec2,
|
||||
glyph_subpaths: Vec<Subpath<PointId>>,
|
||||
pub vector_table: Table<Vector>,
|
||||
scale: f64,
|
||||
id: PointId,
|
||||
}
|
||||
|
||||
impl PathBuilder {
|
||||
pub fn new(per_glyph_instances: bool, scale: f64) -> Self {
|
||||
Self {
|
||||
current_subpath: Subpath::new(Vec::new(), false),
|
||||
glyph_subpaths: Vec::new(),
|
||||
vector_table: if per_glyph_instances { Table::new() } else { Table::new_from_element(Vector::default()) },
|
||||
scale,
|
||||
id: PointId::ZERO,
|
||||
origin: DVec2::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
||||
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], glyph_offset: DVec2, style_skew: Option<DAffine2>, skew: DAffine2, per_glyph_instances: bool) {
|
||||
let location_ref = LocationRef::new(normalized_coords);
|
||||
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
|
||||
glyph.draw(settings, self).unwrap();
|
||||
|
||||
// Apply transforms in correct order: style-based skew first, then user-requested skew
|
||||
// This ensures font synthesis (italic) is applied before user transformations
|
||||
for glyph_subpath in &mut self.glyph_subpaths {
|
||||
if let Some(style_skew) = style_skew {
|
||||
glyph_subpath.apply_transform(style_skew);
|
||||
}
|
||||
|
||||
glyph_subpath.apply_transform(skew);
|
||||
}
|
||||
|
||||
if per_glyph_instances {
|
||||
self.vector_table.push(TableRow {
|
||||
element: Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
|
||||
transform: DAffine2::from_translation(glyph_offset),
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
for subpath in self.glyph_subpaths.drain(..) {
|
||||
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element
|
||||
self.vector_table.get_mut(0).unwrap().element.append_subpath(subpath, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_glyph_run(&mut self, glyph_run: &GlyphRun<'_, ()>, tilt: f64, per_glyph_instances: bool) {
|
||||
let mut run_x = glyph_run.offset();
|
||||
let run_y = glyph_run.baseline();
|
||||
|
||||
let run = glyph_run.run();
|
||||
|
||||
// User-requested tilt applied around baseline to avoid vertical displacement
|
||||
// Translation ensures rotation point is at the baseline, not origin
|
||||
let skew = if per_glyph_instances {
|
||||
DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
||||
} else {
|
||||
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
||||
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
||||
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
||||
};
|
||||
|
||||
let synthesis = run.synthesis();
|
||||
|
||||
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
|
||||
// This preserves the distinction between font styling and user transformations
|
||||
let style_skew = synthesis.skew().map(|angle| {
|
||||
if per_glyph_instances {
|
||||
DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
||||
} else {
|
||||
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
||||
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
||||
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
||||
}
|
||||
});
|
||||
|
||||
let font = run.font();
|
||||
let font_size = run.font_size();
|
||||
|
||||
let normalized_coords = run.normalized_coords().iter().map(|coord| NormalizedCoord::from_bits(*coord)).collect::<Vec<_>>();
|
||||
|
||||
// TODO: This can be cached for better performance
|
||||
let font_collection_ref = font.data.as_ref();
|
||||
let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap();
|
||||
let outlines = font_ref.outline_glyphs();
|
||||
|
||||
for glyph in glyph_run.glyphs() {
|
||||
let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
|
||||
run_x += glyph.advance;
|
||||
|
||||
let glyph_id = GlyphId::from(glyph.id);
|
||||
if let Some(glyph_outline) = outlines.get(glyph_id) {
|
||||
if !per_glyph_instances {
|
||||
self.origin = glyph_offset;
|
||||
}
|
||||
self.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize(mut self) -> Table<Vector> {
|
||||
if self.vector_table.is_empty() {
|
||||
self.vector_table = Table::new_from_element(Vector::default());
|
||||
}
|
||||
self.vector_table
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlinePen for PathBuilder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
if !self.current_subpath.is_empty() {
|
||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, None, None, self.id.next_id()));
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle1);
|
||||
self.current_subpath
|
||||
.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, Some(handle2), None, self.id.next_id()));
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.current_subpath.set_closed(true);
|
||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
}
|
||||
129
node-graph/gcore/src/text/text_context.rs
Normal file
129
node-graph/gcore/src/text/text_context.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use super::{Font, FontCache, TypesettingConfig};
|
||||
use crate::table::Table;
|
||||
use crate::vector::Vector;
|
||||
use core::cell::RefCell;
|
||||
use glam::DVec2;
|
||||
use parley::fontique::{Blob, FamilyId, FontInfo};
|
||||
use parley::{AlignmentOptions, FontContext, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::path_builder::PathBuilder;
|
||||
|
||||
thread_local! {
|
||||
static THREAD_TEXT: RefCell<TextContext> = RefCell::new(TextContext::default());
|
||||
}
|
||||
|
||||
/// Unified thread-local text processing context that combines font and layout management
|
||||
/// for efficient text rendering operations.
|
||||
#[derive(Default)]
|
||||
pub struct TextContext {
|
||||
font_context: FontContext,
|
||||
layout_context: LayoutContext<()>,
|
||||
/// Cached font metadata for performance optimization
|
||||
font_info_cache: HashMap<Font, (FamilyId, FontInfo)>,
|
||||
}
|
||||
|
||||
impl TextContext {
|
||||
/// Access the thread-local TextContext instance for text processing operations
|
||||
pub fn with_thread_local<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut TextContext) -> R,
|
||||
{
|
||||
THREAD_TEXT.with_borrow_mut(f)
|
||||
}
|
||||
|
||||
/// Resolve a font and return its data as a Blob if available
|
||||
fn resolve_font_data<'a>(&self, font: &'a Font, font_cache: &'a FontCache) -> Option<(Blob<u8>, &'a Font)> {
|
||||
font_cache.get_blob(font)
|
||||
}
|
||||
|
||||
/// Get or cache font information for a given font
|
||||
fn get_font_info(&mut self, font: &Font, font_data: &Blob<u8>) -> Option<(String, FontInfo)> {
|
||||
// Check if we already have the font info cached
|
||||
if let Some((family_id, font_info)) = self.font_info_cache.get(font) {
|
||||
if let Some(family_name) = self.font_context.collection.family_name(*family_id) {
|
||||
return Some((family_name.to_string(), font_info.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Register the font and cache the info
|
||||
let families = self.font_context.collection.register_fonts(font_data.clone(), None);
|
||||
|
||||
families.first().and_then(|(family_id, fonts_info)| {
|
||||
fonts_info.first().and_then(|font_info| {
|
||||
self.font_context.collection.family_name(*family_id).map(|family_name| {
|
||||
// Cache the font info for future use
|
||||
self.font_info_cache.insert(font.clone(), (*family_id, font_info.clone()));
|
||||
(family_name.to_string(), font_info.clone())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a text layout using the specified font and typesetting configuration
|
||||
fn layout_text(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> Option<Layout<()>> {
|
||||
// Note that the actual_font may not be the desired font if that font is not yet loaded.
|
||||
// It is important not to cache the default font under the name of another font.
|
||||
let (font_data, actual_font) = self.resolve_font_data(font, font_cache)?;
|
||||
let (font_family, font_info) = self.get_font_info(actual_font, &font_data)?;
|
||||
|
||||
const DISPLAY_SCALE: f32 = 1.;
|
||||
let mut builder = self.layout_context.ranged_builder(&mut self.font_context, text, DISPLAY_SCALE, false);
|
||||
|
||||
builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32));
|
||||
builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32));
|
||||
builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(std::borrow::Cow::Owned(font_family)))));
|
||||
builder.push_default(StyleProperty::FontWeight(font_info.weight()));
|
||||
builder.push_default(StyleProperty::FontStyle(font_info.style()));
|
||||
builder.push_default(StyleProperty::FontWidth(font_info.width()));
|
||||
builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32));
|
||||
|
||||
let mut layout: Layout<()> = builder.build(text);
|
||||
|
||||
layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
|
||||
layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default());
|
||||
|
||||
Some(layout)
|
||||
}
|
||||
|
||||
/// Convert text to vector paths using the specified font and typesetting configuration
|
||||
pub fn to_path(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
|
||||
let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
|
||||
return Table::new_from_element(Vector::default());
|
||||
};
|
||||
|
||||
let mut path_builder = PathBuilder::new(per_glyph_instances, layout.scale() as f64);
|
||||
|
||||
for line in layout.lines() {
|
||||
for item in line.items() {
|
||||
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
|
||||
path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_instances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
path_builder.finalize()
|
||||
}
|
||||
|
||||
/// Calculate the bounding box of text using the specified font and typesetting configuration
|
||||
pub fn bounding_box(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||
if !for_clipping_test {
|
||||
if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) {
|
||||
return DVec2::new(max_width, max_height);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(layout) = self.layout_text(text, font, font_cache, typesetting) else {
|
||||
return DVec2::ZERO;
|
||||
};
|
||||
|
||||
DVec2::new(layout.full_width() as f64, layout.height() as f64)
|
||||
}
|
||||
|
||||
/// Check if text lines are being clipped due to height constraints
|
||||
pub fn lines_clipping(&mut self, text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> bool {
|
||||
let Some(max_height) = typesetting.max_height else { return false };
|
||||
let bounds = self.bounding_box(text, font, font_cache, typesetting, true);
|
||||
max_height < bounds.y
|
||||
}
|
||||
}
|
||||
@@ -1,261 +1,23 @@
|
||||
use super::TextAlign;
|
||||
use crate::subpath::{ManipulatorGroup, Subpath};
|
||||
use crate::table::{Table, TableRow};
|
||||
use crate::vector::{PointId, Vector};
|
||||
use core::cell::RefCell;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use super::text_context::TextContext;
|
||||
use super::{Font, FontCache, TypesettingConfig};
|
||||
use crate::table::Table;
|
||||
use crate::vector::Vector;
|
||||
use glam::DVec2;
|
||||
use parley::fontique::Blob;
|
||||
use parley::{AlignmentOptions, FontContext, GlyphRun, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty};
|
||||
use skrifa::GlyphId;
|
||||
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
|
||||
use skrifa::outline::{DrawSettings, OutlinePen};
|
||||
use skrifa::raw::FontRef as ReadFontsRef;
|
||||
use skrifa::{MetadataProvider, OutlineGlyph};
|
||||
use std::sync::Arc;
|
||||
|
||||
// Thread-local storage avoids expensive re-initialization of font and layout contexts
|
||||
// across multiple text rendering operations within the same thread
|
||||
thread_local! {
|
||||
static FONT_CONTEXT: RefCell<FontContext> = RefCell::new(FontContext::new());
|
||||
static LAYOUT_CONTEXT: RefCell<LayoutContext<()>> = RefCell::new(LayoutContext::new());
|
||||
pub fn to_path(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
|
||||
TextContext::with_thread_local(|ctx| ctx.to_path(text, font, font_cache, typesetting, per_glyph_instances))
|
||||
}
|
||||
|
||||
struct PathBuilder {
|
||||
current_subpath: Subpath<PointId>,
|
||||
origin: DVec2,
|
||||
glyph_subpaths: Vec<Subpath<PointId>>,
|
||||
vector_table: Table<Vector>,
|
||||
scale: f64,
|
||||
id: PointId,
|
||||
}
|
||||
|
||||
impl PathBuilder {
|
||||
fn point(&self, x: f32, y: f32) -> DVec2 {
|
||||
DVec2::new(self.origin.x + x as f64, self.origin.y - y as f64) * self.scale
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_glyph(&mut self, glyph: &OutlineGlyph<'_>, size: f32, normalized_coords: &[NormalizedCoord], glyph_offset: DVec2, style_skew: Option<DAffine2>, skew: DAffine2, per_glyph_instances: bool) {
|
||||
let location_ref = LocationRef::new(normalized_coords);
|
||||
let settings = DrawSettings::unhinted(Size::new(size), location_ref);
|
||||
glyph.draw(settings, self).unwrap();
|
||||
|
||||
// Apply transforms in correct order: style-based skew first, then user-requested skew
|
||||
// This ensures font synthesis (italic) is applied before user transformations
|
||||
for glyph_subpath in &mut self.glyph_subpaths {
|
||||
if let Some(style_skew) = style_skew {
|
||||
glyph_subpath.apply_transform(style_skew);
|
||||
}
|
||||
|
||||
glyph_subpath.apply_transform(skew);
|
||||
}
|
||||
|
||||
if per_glyph_instances {
|
||||
self.vector_table.push(TableRow {
|
||||
element: Vector::from_subpaths(core::mem::take(&mut self.glyph_subpaths), false),
|
||||
transform: DAffine2::from_translation(glyph_offset),
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
for subpath in self.glyph_subpaths.drain(..) {
|
||||
// Unwrapping here is ok because `self.vector_table` is initialized with a single `Vector` table element
|
||||
self.vector_table.get_mut(0).unwrap().element.append_subpath(subpath, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlinePen for PathBuilder {
|
||||
fn move_to(&mut self, x: f32, y: f32) {
|
||||
if !self.current_subpath.is_empty() {
|
||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||
}
|
||||
|
||||
fn line_to(&mut self, x: f32, y: f32) {
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_anchor_with_id(self.point(x, y), self.id.next_id()));
|
||||
}
|
||||
|
||||
fn quad_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) {
|
||||
let [handle, anchor] = [self.point(x1, y1), self.point(x2, y2)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle);
|
||||
self.current_subpath.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, None, None, self.id.next_id()));
|
||||
}
|
||||
|
||||
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
|
||||
let [handle1, handle2, anchor] = [self.point(x1, y1), self.point(x2, y2), self.point(x3, y3)];
|
||||
self.current_subpath.last_manipulator_group_mut().unwrap().out_handle = Some(handle1);
|
||||
self.current_subpath
|
||||
.push_manipulator_group(ManipulatorGroup::new_with_id(anchor, Some(handle2), None, self.id.next_id()));
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
self.current_subpath.set_closed(true);
|
||||
self.glyph_subpaths.push(std::mem::replace(&mut self.current_subpath, Subpath::new(Vec::new(), false)));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TypesettingConfig {
|
||||
pub font_size: f64,
|
||||
pub line_height_ratio: f64,
|
||||
pub character_spacing: f64,
|
||||
pub max_width: Option<f64>,
|
||||
pub max_height: Option<f64>,
|
||||
pub tilt: f64,
|
||||
pub align: TextAlign,
|
||||
}
|
||||
|
||||
impl Default for TypesettingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
font_size: 24.,
|
||||
line_height_ratio: 1.2,
|
||||
character_spacing: 0.,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
tilt: 0.,
|
||||
align: TextAlign::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_glyph_run(glyph_run: &GlyphRun<'_, ()>, path_builder: &mut PathBuilder, tilt: f64, per_glyph_instances: bool) {
|
||||
let mut run_x = glyph_run.offset();
|
||||
let run_y = glyph_run.baseline();
|
||||
|
||||
let run = glyph_run.run();
|
||||
|
||||
// User-requested tilt applied around baseline to avoid vertical displacement
|
||||
// Translation ensures rotation point is at the baseline, not origin
|
||||
let skew = if per_glyph_instances {
|
||||
DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
||||
} else {
|
||||
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
||||
* DAffine2::from_cols_array(&[1., 0., -tilt.to_radians().tan(), 1., 0., 0.])
|
||||
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
||||
};
|
||||
|
||||
let synthesis = run.synthesis();
|
||||
|
||||
// Font synthesis (e.g., synthetic italic) applied separately from user transforms
|
||||
// This preserves the distinction between font styling and user transformations
|
||||
let style_skew = synthesis.skew().map(|angle| {
|
||||
if per_glyph_instances {
|
||||
DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
||||
} else {
|
||||
DAffine2::from_translation(DVec2::new(0., run_y as f64))
|
||||
* DAffine2::from_cols_array(&[1., 0., -angle.to_radians().tan() as f64, 1., 0., 0.])
|
||||
* DAffine2::from_translation(DVec2::new(0., -run_y as f64))
|
||||
}
|
||||
});
|
||||
|
||||
let font = run.font();
|
||||
let font_size = run.font_size();
|
||||
|
||||
let normalized_coords = run.normalized_coords().iter().map(|coord| NormalizedCoord::from_bits(*coord)).collect::<Vec<_>>();
|
||||
|
||||
// TODO: This can be cached for better performance
|
||||
let font_collection_ref = font.data.as_ref();
|
||||
let font_ref = ReadFontsRef::from_index(font_collection_ref, font.index).unwrap();
|
||||
let outlines = font_ref.outline_glyphs();
|
||||
|
||||
for glyph in glyph_run.glyphs() {
|
||||
let glyph_offset = DVec2::new((run_x + glyph.x) as f64, (run_y - glyph.y) as f64);
|
||||
run_x += glyph.advance;
|
||||
|
||||
let glyph_id = GlyphId::from(glyph.id);
|
||||
if let Some(glyph_outline) = outlines.get(glyph_id) {
|
||||
if !per_glyph_instances {
|
||||
path_builder.origin = glyph_offset;
|
||||
}
|
||||
path_builder.draw_glyph(&glyph_outline, font_size, &normalized_coords, glyph_offset, style_skew, skew, per_glyph_instances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_text(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> Option<Layout<()>> {
|
||||
FONT_CONTEXT.with_borrow_mut(|mut font_cx| {
|
||||
LAYOUT_CONTEXT.with_borrow_mut(|layout_cx| {
|
||||
let (font_family, font_info) = font_data.and_then(|font_data| {
|
||||
let families = font_cx.collection.register_fonts(font_data, None);
|
||||
|
||||
families.first().and_then(|(family_id, fonts_info)| {
|
||||
fonts_info
|
||||
.first()
|
||||
.and_then(|font_info| font_cx.collection.family_name(*family_id).map(|f| (f.to_string(), font_info.clone())))
|
||||
})
|
||||
})?;
|
||||
|
||||
const DISPLAY_SCALE: f32 = 1.;
|
||||
let mut builder = layout_cx.ranged_builder(&mut font_cx, str, DISPLAY_SCALE, false);
|
||||
|
||||
builder.push_default(StyleProperty::FontSize(typesetting.font_size as f32));
|
||||
builder.push_default(StyleProperty::LetterSpacing(typesetting.character_spacing as f32));
|
||||
builder.push_default(StyleProperty::FontStack(parley::FontStack::Single(parley::FontFamily::Named(std::borrow::Cow::Owned(font_family)))));
|
||||
builder.push_default(StyleProperty::FontWeight(font_info.weight()));
|
||||
builder.push_default(StyleProperty::FontStyle(font_info.style()));
|
||||
builder.push_default(StyleProperty::FontWidth(font_info.width()));
|
||||
builder.push_default(LineHeight::FontSizeRelative(typesetting.line_height_ratio as f32));
|
||||
|
||||
let mut layout: Layout<()> = builder.build(str);
|
||||
|
||||
layout.break_all_lines(typesetting.max_width.map(|mw| mw as f32));
|
||||
layout.align(typesetting.max_width.map(|max_w| max_w as f32), typesetting.align.into(), AlignmentOptions::default());
|
||||
|
||||
Some(layout)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_path(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, per_glyph_instances: bool) -> Table<Vector> {
|
||||
let Some(layout) = layout_text(str, font_data, typesetting) else {
|
||||
return Table::new_from_element(Vector::default());
|
||||
};
|
||||
|
||||
let mut path_builder = PathBuilder {
|
||||
current_subpath: Subpath::new(Vec::new(), false),
|
||||
glyph_subpaths: Vec::new(),
|
||||
vector_table: if per_glyph_instances { Table::new() } else { Table::new_from_element(Vector::default()) },
|
||||
scale: layout.scale() as f64,
|
||||
id: PointId::ZERO,
|
||||
origin: DVec2::default(),
|
||||
};
|
||||
|
||||
for line in layout.lines() {
|
||||
for item in line.items() {
|
||||
if let PositionedLayoutItem::GlyphRun(glyph_run) = item {
|
||||
render_glyph_run(&glyph_run, &mut path_builder, typesetting.tilt, per_glyph_instances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if path_builder.vector_table.is_empty() {
|
||||
path_builder.vector_table = Table::new_from_element(Vector::default());
|
||||
}
|
||||
|
||||
path_builder.vector_table
|
||||
}
|
||||
|
||||
pub fn bounding_box(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||
if !for_clipping_test {
|
||||
if let (Some(max_height), Some(max_width)) = (typesetting.max_height, typesetting.max_width) {
|
||||
return DVec2::new(max_width, max_height);
|
||||
}
|
||||
}
|
||||
|
||||
let Some(layout) = layout_text(str, font_data, typesetting) else { return DVec2::ZERO };
|
||||
|
||||
DVec2::new(layout.full_width() as f64, layout.height() as f64)
|
||||
pub fn bounding_box(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig, for_clipping_test: bool) -> DVec2 {
|
||||
TextContext::with_thread_local(|ctx| ctx.bounding_box(text, font, font_cache, typesetting, for_clipping_test))
|
||||
}
|
||||
|
||||
pub fn load_font(data: &[u8]) -> Blob<u8> {
|
||||
Blob::new(Arc::new(data.to_vec()))
|
||||
}
|
||||
|
||||
pub fn lines_clipping(str: &str, font_data: Option<Blob<u8>>, typesetting: TypesettingConfig) -> bool {
|
||||
let Some(max_height) = typesetting.max_height else { return false };
|
||||
let bounds = bounding_box(str, font_data, typesetting, true);
|
||||
max_height < bounds.y
|
||||
pub fn lines_clipping(text: &str, font: &Font, font_cache: &FontCache, typesetting: TypesettingConfig) -> bool {
|
||||
TextContext::with_thread_local(|ctx| ctx.lines_clipping(text, font, font_cache, typesetting))
|
||||
}
|
||||
|
||||
@@ -38,7 +38,5 @@ fn text<'i: 'n>(
|
||||
align,
|
||||
};
|
||||
|
||||
let font_data = editor.font_cache.get(&font_name).map(|f| load_font(f));
|
||||
|
||||
to_path(&text, font_data, typesetting, per_glyph_instances)
|
||||
to_path(&text, &font_name, &editor.font_cache, typesetting, per_glyph_instances)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user