use super::{Font, FontCache, TypesettingConfig}; use core::cell::RefCell; use core_types::table::Table; use glam::DVec2; use parley::fontique::{Blob, FamilyId, FontInfo}; use parley::{AlignmentOptions, FontContext, Layout, LayoutContext, LineHeight, PositionedLayoutItem, StyleProperty}; use std::collections::HashMap; use vector_types::Vector; use super::path_builder::PathBuilder; thread_local! { static THREAD_TEXT: RefCell = 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, } impl TextContext { /// Access the thread-local TextContext instance for text processing operations pub fn with_thread_local(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, &'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) -> 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) && 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> { // 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> { 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 && 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 } }