1use std::{
2 borrow::Cow,
3 hash::{Hash, Hasher},
4 ops::ControlFlow,
5 sync::{Arc, Mutex, MutexGuard, PoisonError},
6};
7
8use ab_glyph::{
9 Font, FontArc, FontRef, FontVec, Glyph, GlyphId, InvalidFont, OutlinedGlyph, PxScale,
10 ScaleFont, VariableFont, point,
11};
12use cranpose_core::{
13 collections::{bounded_lru::BoundedLruCache, pass_aged::PassAgedCache},
14 hash::default as default_hash,
15};
16use cranpose_ui::{
17 TextLinePrefixWidths, TextMeasurer, TextMetrics,
18 text::{
19 AnnotatedString, FontFamily, FontStyle, FontSynthesis, FontWeight, RangeStyle,
20 RenderString, Shadow, SpanStyle, TextDrawStyle, TextMotion, TextShaping, TextStyle,
21 },
22 text_layout_result::{GlyphLayout, LineLayout, TextLayoutData, TextLayoutResult},
23};
24use cranpose_ui_graphics::{Color, ImageBitmap, Point, Rect};
25use tiny_skia::{LineCap, LineJoin, Paint, Path, PathBuilder, Pixmap, Stroke, Transform};
26use unicode_segmentation::UnicodeSegmentation;
27
28#[cfg(test)]
29use crate::font_layout::layout_line_glyphs;
30#[cfg(feature = "text-hyphenation")]
31use crate::text_hyphenation::HyphenationDictionaryError;
32use crate::{
33 Brush,
34 annotated_text::{
35 AnnotatedBrushExtent, AnnotatedTextLayout, AnnotatedTextSegment, FontResolver,
36 },
37 ascii_glyphs::{ASCII_COUNT, ascii_slot},
38 brush_sampling::{color_to_rgba, sample_brush_rgba},
39 direct_mapped_cache::DirectMappedCache,
40 font_layout::{
41 GlyphPixelBounds, align_glyph_to_pixel_grid, line_advance_width,
42 pixel_bounds_from_outlined, vertical_metrics,
43 },
44 font_tracking::FontTracking,
45 gpos_kerning::KernedFont,
46 text_cache_key::{TextCacheKey, TextProbe},
47 text_hyphenation::HyphenationDictionaryStore,
48 text_mask_gamma::TextLuminance,
49};
50
51const COMPOSE_STROKE_MITER_LIMIT: f32 = 4.0;
52const SHADOW_SIGMA_SCALE: f32 = 0.57735;
53const SHADOW_SIGMA_BIAS: f32 = 0.5;
54const MAX_GAUSSIAN_KERNEL_HALF: i32 = 128;
55const SOFTWARE_TEXT_GLYPH_METRICS_SLOTS_LOG2: u32 = 11;
58const SOFTWARE_TEXT_KERN_METRICS_SLOTS_LOG2: u32 = 13;
60#[cfg(feature = "embedded-default-font")]
61#[doc(hidden)]
62pub const DEFAULT_SOFTWARE_TEXT_FONT_BYTES: &[u8] = include_bytes!("../assets/NotoSansMerged.ttf");
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
65pub enum SoftwareTextFontError {
66 #[error("invalid software text font bytes")]
67 InvalidFont,
68 #[error("invalid font variation axis {tag:?}")]
70 InvalidVariation {
71 tag: [u8; 4],
73 },
74 #[error("embedded default font disabled (feature `embedded-default-font` is off)")]
75 EmbeddedFontDisabled,
76}
77
78#[derive(Clone)]
79pub struct SoftwareTextFont {
80 font: KernedFont,
81 metadata: SoftwareTextFontMetadata,
82 score: TextFontScore,
83 content_hash: u64,
84 glyph_map_hash: u64,
85 #[cfg(feature = "text-shaping")]
86 shaping: Arc<Mutex<crate::text_shaping::ShapingCache>>,
87}
88
89#[derive(Clone)]
90struct SoftwareTextFontMetadata {
91 families: Arc<[String]>,
92 registered_family: Option<FontFamilyKey>,
93 weight: FontWeight,
94 style: FontStyle,
95 ab_glyph_scale_factor: f32,
96 tracking: FontTracking,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
107pub struct FontFamilyKey(u64);
108
109impl FontFamilyKey {
110 pub fn of(family: &FontFamily) -> Self {
111 let mut state = default_hash::new();
112 family.hash(&mut state);
113 Self(state.finish())
114 }
115}
116
117pub enum FontBytes {
119 Static(&'static [u8]),
122 Owned(Vec<u8>),
124}
125
126impl FontBytes {
127 pub fn as_slice(&self) -> &[u8] {
129 match self {
130 Self::Static(bytes) => bytes,
131 Self::Owned(bytes) => bytes,
132 }
133 }
134
135 fn into_font(self) -> Result<FontArc, InvalidFont> {
136 match self {
137 Self::Static(bytes) => FontArc::try_from_slice(bytes),
138 Self::Owned(bytes) => FontArc::try_from_vec(bytes),
139 }
140 }
141}
142
143impl From<&'static [u8]> for FontBytes {
144 fn from(bytes: &'static [u8]) -> Self {
145 Self::Static(bytes)
146 }
147}
148
149impl<const N: usize> From<&'static [u8; N]> for FontBytes {
150 fn from(bytes: &'static [u8; N]) -> Self {
151 Self::Static(bytes)
152 }
153}
154
155impl From<Vec<u8>> for FontBytes {
156 fn from(bytes: Vec<u8>) -> Self {
157 Self::Owned(bytes)
158 }
159}
160
161struct InstancedFace {
163 font: FontArc,
164 kerning: Option<Arc<crate::gpos_kerning::GposKerning>>,
165 variations: Vec<([u8; 4], f32)>,
166}
167
168impl SoftwareTextFont {
169 pub fn from_bytes(bytes: impl Into<FontBytes>) -> Result<Self, SoftwareTextFontError> {
170 let bytes = bytes.into();
171 let mut hasher = default_hash::new();
172 bytes.as_slice().hash(&mut hasher);
173 let content_hash = hasher.finish();
174 let metadata = software_text_font_metadata(bytes.as_slice());
175 let kerning = KernedFont::read_kerning(bytes.as_slice(), &[]);
176 let font = bytes
177 .into_font()
178 .map_err(|_| SoftwareTextFontError::InvalidFont)?;
179 let score = text_font_score_from_parts(&font, &metadata);
180 Ok(Self {
181 font: KernedFont::new(font, kerning),
182 metadata,
183 score,
184 content_hash,
185 glyph_map_hash: content_hash,
186 #[cfg(feature = "text-shaping")]
187 shaping: Arc::new(Mutex::new(crate::text_shaping::ShapingCache::new(&[]))),
188 })
189 }
190
191 pub fn from_registered_bytes(
201 family: &FontFamily,
202 weight: FontWeight,
203 style: FontStyle,
204 bytes: impl Into<FontBytes>,
205 ) -> Result<Self, SoftwareTextFontError> {
206 Self::from_registered_bytes_with_variations(family, weight, style, bytes, &[])
207 }
208
209 pub fn from_registered_bytes_with_variations(
213 family: &FontFamily,
214 weight: FontWeight,
215 style: FontStyle,
216 bytes: impl Into<FontBytes>,
217 variations: &[([u8; 4], f32)],
218 ) -> Result<Self, SoftwareTextFontError> {
219 let bytes = bytes.into();
220 let mut hasher = default_hash::new();
221 bytes.as_slice().hash(&mut hasher);
222 let mut metadata = software_text_font_metadata(bytes.as_slice());
223 metadata.registered_family = Some(FontFamilyKey::of(family));
224 metadata.weight = weight;
225 metadata.style = style;
226
227 let invalid = |_| SoftwareTextFontError::InvalidFont;
228 let InstancedFace {
229 font,
230 kerning,
231 variations,
232 } = match bytes {
233 FontBytes::Static(bytes) => instance_face(
234 FontRef::try_from_slice(bytes).map_err(invalid)?,
235 weight,
236 style,
237 variations,
238 ),
239 FontBytes::Owned(bytes) => instance_face(
240 FontVec::try_from_vec(bytes).map_err(invalid)?,
241 weight,
242 style,
243 variations,
244 ),
245 }?;
246 for (tag, value) in &variations {
247 tag.hash(&mut hasher);
248 value.to_bits().hash(&mut hasher);
249 }
250 let content_hash = hasher.finish();
251
252 let score = text_font_score_from_parts(&font, &metadata);
253 Ok(Self {
254 font: KernedFont::new(font, kerning),
255 metadata,
256 score,
257 content_hash,
258 glyph_map_hash: content_hash,
259 #[cfg(feature = "text-shaping")]
260 shaping: Arc::new(Mutex::new(crate::text_shaping::ShapingCache::new(
261 &variations,
262 ))),
263 })
264 }
265
266 pub fn family_names(&self) -> &[String] {
267 &self.metadata.families
268 }
269
270 pub fn registered_family(&self) -> Option<FontFamilyKey> {
272 self.metadata.registered_family
273 }
274
275 pub fn weight(&self) -> FontWeight {
276 self.metadata.weight
277 }
278
279 pub fn style(&self) -> FontStyle {
280 self.metadata.style
281 }
282
283 fn ab_glyph_px_size(&self, logical_font_size: f32) -> f32 {
284 logical_font_size * self.metadata.ab_glyph_scale_factor
285 }
286
287 pub fn content_hash(&self) -> u64 {
290 self.content_hash
291 }
292
293 pub(crate) fn shaped_for(&self, style: &TextStyle) -> Cow<'_, Self> {
294 match self
295 .font
296 .with_feature_settings(style.span_style.font_feature_settings.as_deref())
297 {
298 None => Cow::Borrowed(self),
299 Some(font) => Cow::Owned(Self {
300 glyph_map_hash: self.content_hash ^ font.feature_key(),
301 font,
302 metadata: self.metadata.clone(),
303 score: self.score,
304 content_hash: self.content_hash,
305 #[cfg(feature = "text-shaping")]
306 shaping: Arc::clone(&self.shaping),
307 }),
308 }
309 }
310
311 fn takes_ascii(&self, text: &str) -> bool {
317 text.is_ascii()
318 && text.bytes().map(char::from).all(|ch| {
319 ch.is_ascii_control()
320 || self
321 .font
322 .ascii_glyph(ch)
323 .is_some_and(|(glyph, _)| glyph.0 != 0)
324 })
325 }
326
327 fn raster_ref(&self) -> RasterFontRef<'_, KernedFont> {
328 RasterFontRef {
329 font: &self.font,
330 ab_glyph_scale_factor: self.metadata.ab_glyph_scale_factor,
331 weight: self.weight(),
332 style: self.style(),
333 tracking: &self.metadata.tracking,
334 }
335 }
336
337 #[cfg(feature = "text-shaping")]
338 fn shaped_run(
339 &self,
340 text: &str,
341 style: &TextStyle,
342 ) -> Option<Arc<crate::text_shaping::ShapedRun>> {
343 self.shaping
344 .lock()
345 .unwrap_or_else(PoisonError::into_inner)
346 .shape(self.font.font_data(), text, style)
347 }
348}
349
350pub fn try_default_software_text_font() -> Result<SoftwareTextFont, SoftwareTextFontError> {
351 #[cfg(feature = "embedded-default-font")]
352 {
353 SoftwareTextFont::from_bytes(DEFAULT_SOFTWARE_TEXT_FONT_BYTES)
354 }
355 #[cfg(not(feature = "embedded-default-font"))]
356 {
357 Err(SoftwareTextFontError::EmbeddedFontDisabled)
358 }
359}
360
361pub fn default_software_text_font() -> Option<SoftwareTextFont> {
362 try_default_software_text_font().ok()
363}
364
365#[derive(Clone)]
366pub struct SoftwareTextFontSet {
367 fonts: Arc<[SoftwareTextFont]>,
368 registered_families: Arc<[FontFamilyKey]>,
369 default_index: Option<usize>,
370}
371
372impl SoftwareTextFontSet {
373 pub fn empty() -> Self {
374 Self::from_faces(Vec::new())
375 }
376
377 pub fn from_font(font: SoftwareTextFont) -> Self {
378 Self::from_faces(vec![font])
379 }
380
381 pub fn from_faces(fonts: Vec<SoftwareTextFont>) -> Self {
384 let mut registered_families: Vec<FontFamilyKey> = Vec::new();
385 for family in fonts.iter().filter_map(SoftwareTextFont::registered_family) {
386 if !registered_families.contains(&family) {
387 registered_families.push(family);
388 }
389 }
390 let default_index = (!fonts.is_empty()).then(|| default_font_index(&fonts));
391 Self {
392 fonts: Arc::from(fonts),
393 registered_families: Arc::from(registered_families),
394 default_index,
395 }
396 }
397
398 pub fn from_fonts_or_default(fonts: &[&'static [u8]]) -> Self {
399 let mut parsed = Vec::with_capacity(fonts.len().max(1));
400 for font in fonts {
401 if let Ok(candidate) = SoftwareTextFont::from_bytes(*font) {
402 parsed.push(candidate);
403 }
404 }
405 if parsed.is_empty()
406 && let Some(default_font) = default_software_text_font()
407 {
408 parsed.push(default_font);
409 }
410
411 Self::from_faces(parsed)
412 }
413
414 pub fn default_font(&self) -> Option<&SoftwareTextFont> {
415 self.default_index.and_then(|index| self.fonts.get(index))
416 }
417
418 pub fn faces(&self) -> &[SoftwareTextFont] {
420 &self.fonts
421 }
422
423 pub fn has_registered_family(&self, family: &FontFamily) -> bool {
425 self.registered_families
426 .contains(&FontFamilyKey::of(family))
427 }
428
429 pub fn resolve(&self, style: &TextStyle) -> Option<&SoftwareTextFont> {
433 self.resolve_for_request(FontSelectionRequest::resolve(
434 style,
435 &self.registered_families,
436 ))
437 }
438
439 fn resolve_for_request(&self, request: FontSelectionRequest<'_>) -> Option<&SoftwareTextFont> {
440 let mut best: Option<(usize, FontMatchScore)> = None;
441 for (index, font) in self.fonts.iter().enumerate() {
442 let Some(score) = font_match_score(font, request) else {
443 continue;
444 };
445 if best.is_none_or(|(_, best_score)| score < best_score) {
446 best = Some((index, score));
447 }
448 }
449
450 let index = best.map(|(index, _)| index).or(self.default_index);
451 index.and_then(|index| self.fonts.get(index))
452 }
453
454 fn resolve_grapheme<'a>(
455 &'a self,
456 primary: &'a SoftwareTextFont,
457 grapheme: &str,
458 request: FontSelectionRequest<'_>,
459 ) -> &'a SoftwareTextFont {
460 if font_supports_grapheme(primary, grapheme) {
461 return primary;
462 }
463
464 self.fonts
465 .iter()
466 .enumerate()
467 .filter(|(_, font)| font_supports_grapheme(font, grapheme))
468 .min_by_key(|(index, font)| {
469 (
470 u8::from(!request.family.matches(font)),
471 font_match_score_unfiltered(font, request.target_weight, request.target_style),
472 *index,
473 )
474 })
475 .map_or(primary, |(_, font)| font)
476 }
477
478 pub(crate) fn visit_font_runs<'a>(
479 &'a self,
480 text: &'a str,
481 style: &TextStyle,
482 mut visit: impl FnMut(std::ops::Range<usize>, &'a SoftwareTextFont),
483 ) -> Option<()> {
484 let request = FontSelectionRequest::resolve(style, &self.registered_families);
485 let primary = self.resolve_for_request(request)?;
486 if self.fonts.len() == 1 || primary.takes_ascii(text) {
487 if !text.is_empty() {
488 visit(0..text.len(), primary);
489 }
490 return Some(());
491 }
492 let mut run_start = 0;
493 let mut run_font = None;
494 for (start, grapheme) in text.grapheme_indices(true) {
495 let font = self.resolve_grapheme(primary, grapheme, request);
496 match run_font {
497 None => {
498 run_start = start;
499 run_font = Some(font);
500 }
501 Some(previous) if !std::ptr::eq(previous, font) => {
502 visit(run_start..start, previous);
503 run_start = start;
504 run_font = Some(font);
505 }
506 Some(_) => {}
507 }
508 }
509 if let Some(font) = run_font {
510 visit(run_start..text.len(), font);
511 }
512 Some(())
513 }
514
515 pub(crate) fn single_font_for_text(
516 &self,
517 text: &str,
518 style: &TextStyle,
519 ) -> Option<&SoftwareTextFont> {
520 self.resolve_covering(text, style)
521 .and_then(|(primary, covers)| covers.then_some(primary))
522 }
523
524 fn resolve_covering(&self, text: &str, style: &TextStyle) -> Option<(&SoftwareTextFont, bool)> {
525 let request = FontSelectionRequest::resolve(style, &self.registered_families);
526 let primary = self.resolve_for_request(request)?;
527 let covers = self.fonts.len() == 1
528 || primary.takes_ascii(text)
529 || text.graphemes(true).all(|grapheme| {
530 std::ptr::eq(primary, self.resolve_grapheme(primary, grapheme, request))
531 });
532 Some((primary, covers))
533 }
534}
535
536fn font_supports_grapheme(font: &SoftwareTextFont, grapheme: &str) -> bool {
537 grapheme.chars().all(|ch| {
538 ch.is_control()
539 || matches!(ch, '\u{200c}' | '\u{200d}' | '\u{2066}'..='\u{2069}' | '\u{fe00}'..='\u{fe0f}')
540 || font.font.glyph_id(ch).0 != 0
541 })
542}
543
544pub fn software_text_font_from_fonts_or_default(
545 fonts: &[&'static [u8]],
546) -> Option<SoftwareTextFont> {
547 SoftwareTextFontSet::from_fonts_or_default(fonts)
548 .default_font()
549 .cloned()
550}
551
552pub fn software_text_font_set_from_fonts_or_default(
553 fonts: &[&'static [u8]],
554) -> SoftwareTextFontSet {
555 SoftwareTextFontSet::from_fonts_or_default(fonts)
556}
557
558#[derive(Clone, Copy)]
559struct TextFontScore {
560 supported_latin_chars: usize,
561 latin_sample_width: f32,
562}
563
564impl TextFontScore {
565 fn is_complete_default_face(self) -> bool {
566 const LATIN_SAMPLE_CHAR_COUNT: usize = 21;
567 self.supported_latin_chars == LATIN_SAMPLE_CHAR_COUNT && self.latin_sample_width > 1.0
568 }
569
570 fn is_better_than(self, other: Self) -> bool {
571 self.supported_latin_chars > other.supported_latin_chars
572 || (self.supported_latin_chars == other.supported_latin_chars
573 && self.latin_sample_width > other.latin_sample_width)
574 }
575}
576
577fn text_font_score(font: &SoftwareTextFont) -> TextFontScore {
578 font.score
579}
580
581fn text_font_score_from_parts(
582 font: &FontArc,
583 metadata: &SoftwareTextFontMetadata,
584) -> TextFontScore {
585 const SAMPLE: &str = "UNDER The quick brown fox";
586 let glyph_font_size = 18.0 * metadata.ab_glyph_scale_factor;
587 let scaled_font = font.as_scaled(PxScale::from(glyph_font_size));
588 let supported_latin_chars = SAMPLE
589 .chars()
590 .filter(|ch| !ch.is_whitespace())
591 .filter(|ch| scaled_font.glyph_id(*ch).0 != 0)
592 .count();
593 let latin_sample_width = measure_text_impl(
594 SAMPLE,
595 &TextStyle::default(),
596 18.0,
597 RasterFontRef {
598 font,
599 ab_glyph_scale_factor: metadata.ab_glyph_scale_factor,
600 style: metadata.style,
601 weight: metadata.weight,
602 tracking: &metadata.tracking,
603 },
604 )
605 .width;
606 TextFontScore {
607 supported_latin_chars,
608 latin_sample_width,
609 }
610}
611
612fn default_font_index(fonts: &[SoftwareTextFont]) -> usize {
613 let mut best: Option<(usize, TextFontScore)> = None;
614 for (index, font) in fonts.iter().enumerate() {
615 let score = text_font_score(font);
616 if font.style() == FontStyle::Normal
617 && font.weight() == FontWeight::NORMAL
618 && score.is_complete_default_face()
619 {
620 return index;
621 }
622 if best
623 .as_ref()
624 .is_none_or(|(_, best_score)| score.is_better_than(*best_score))
625 {
626 best = Some((index, score));
627 }
628 }
629 best.map_or(0, |(index, _)| index)
630}
631
632#[derive(Clone, Copy)]
633enum FontFamilyRequest<'a> {
634 Any,
635 Named { name: &'a str, key: FontFamilyKey },
636 Registered(FontFamilyKey),
637}
638
639impl<'a> FontFamilyRequest<'a> {
640 fn resolve(font_family: Option<&'a FontFamily>, registered: &[FontFamilyKey]) -> Self {
641 match font_family {
642 None | Some(FontFamily::Default) => Self::Any,
643 Some(family @ FontFamily::Named(name)) => Self::Named {
644 name: name.as_str(),
645 key: FontFamilyKey::of(family),
646 },
647 Some(family @ (FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_))) => {
648 Self::Registered(FontFamilyKey::of(family))
649 }
650 Some(family) => {
651 let key = FontFamilyKey::of(family);
652 if registered.contains(&key) {
653 Self::Registered(key)
654 } else {
655 Self::Any
656 }
657 }
658 }
659 }
660
661 fn matches(self, font: &SoftwareTextFont) -> bool {
662 match self {
663 Self::Any => true,
664 Self::Named { name, key } => {
665 font_family_matches(font, name) || font.registered_family() == Some(key)
666 }
667 Self::Registered(key) => font.registered_family() == Some(key),
668 }
669 }
670}
671
672#[derive(Clone, Copy)]
673struct FontSelectionRequest<'a> {
674 family: FontFamilyRequest<'a>,
675 target_weight: FontWeight,
676 target_style: FontStyle,
677}
678
679impl<'a> FontSelectionRequest<'a> {
680 fn resolve(style: &'a TextStyle, registered: &[FontFamilyKey]) -> Self {
681 Self {
682 family: FontFamilyRequest::resolve(style.span_style.font_family.as_ref(), registered),
683 target_weight: style.span_style.font_weight.unwrap_or_default(),
684 target_style: style.span_style.font_style.unwrap_or_default(),
685 }
686 }
687}
688
689type FontMatchScore = (u32, u8, u16);
690
691fn font_match_score(
692 font: &SoftwareTextFont,
693 request: FontSelectionRequest<'_>,
694) -> Option<FontMatchScore> {
695 if !request.family.matches(font) {
696 return None;
697 }
698 Some(font_match_score_unfiltered(
699 font,
700 request.target_weight,
701 request.target_style,
702 ))
703}
704
705fn font_match_score_unfiltered(
706 font: &SoftwareTextFont,
707 target_weight: FontWeight,
708 target_style: FontStyle,
709) -> FontMatchScore {
710 let style_penalty = if font.style() == target_style {
711 0
712 } else {
713 10_000
714 };
715 let (weight_group, weight_distance) = font_weight_rank(font.weight(), target_weight);
716 let coverage_penalty =
717 (21usize.saturating_sub(text_font_score(font).supported_latin_chars) as u32) * 1_000;
718
719 (
720 style_penalty + coverage_penalty,
721 weight_group,
722 weight_distance,
723 )
724}
725
726fn font_weight_rank(candidate: FontWeight, requested: FontWeight) -> (u8, u16) {
727 let candidate = candidate.value();
728 let requested = requested.value();
729 let group = if requested < 400 {
730 u8::from(candidate > requested)
731 } else if requested > 500 {
732 u8::from(candidate < requested)
733 } else if candidate < requested {
734 1
735 } else if candidate <= 500 {
736 0
737 } else {
738 2
739 };
740 (group, candidate.abs_diff(requested))
741}
742
743fn font_family_matches(font: &SoftwareTextFont, requested: &str) -> bool {
744 font.family_names()
745 .iter()
746 .any(|family| family.eq_ignore_ascii_case(requested))
747}
748
749fn instance_face<F: Font + VariableFont + Send + Sync + 'static>(
752 mut font: F,
753 weight: FontWeight,
754 style: FontStyle,
755 variations: &[([u8; 4], f32)],
756) -> Result<InstancedFace, SoftwareTextFontError> {
757 let mut applied = apply_declared_variations(&mut font, weight, style);
758 for &(tag, value) in variations {
759 let valid = value.is_finite()
760 && font
761 .variations()
762 .iter()
763 .any(|axis| axis.tag == tag && (axis.min_value..=axis.max_value).contains(&value));
764 if !valid || !font.set_variation(&tag, value) {
765 return Err(SoftwareTextFontError::InvalidVariation { tag });
766 }
767 applied.retain(|(existing, _)| *existing != tag);
768 applied.push((tag, value));
769 }
770 applied.sort_unstable_by_key(|(tag, _)| *tag);
771 let kerning = KernedFont::read_kerning(font.font_data(), &applied);
772 Ok(InstancedFace {
773 font: FontArc::new(font),
774 kerning,
775 variations: applied,
776 })
777}
778
779fn apply_declared_variations(
780 font: &mut impl VariableFont,
781 weight: FontWeight,
782 style: FontStyle,
783) -> Vec<([u8; 4], f32)> {
784 const OBLIQUE_DEGREES: f32 = -12.0;
785
786 let mut applied = Vec::new();
787 for axis in font.variations() {
788 let requested = match &axis.tag {
789 b"wght" => f32::from(weight.value()),
790 b"ital" if style == FontStyle::Italic => 1.0,
791 b"slnt" if style == FontStyle::Italic => OBLIQUE_DEGREES,
792 _ => continue,
793 };
794 let value = requested.clamp(axis.min_value, axis.max_value);
795 if font.set_variation(&axis.tag, value) {
796 applied.push((axis.tag, value));
797 }
798 }
799 applied
800}
801
802fn software_text_font_metadata(bytes: &[u8]) -> SoftwareTextFontMetadata {
803 let Some(face) = ttf_parser::Face::parse(bytes, 0).ok() else {
804 return SoftwareTextFontMetadata {
805 families: Arc::from(Vec::<String>::new()),
806 registered_family: None,
807 weight: FontWeight::NORMAL,
808 style: FontStyle::Normal,
809 ab_glyph_scale_factor: 1.0,
810 tracking: FontTracking::default(),
811 };
812 };
813
814 let mut families = Vec::new();
815 for name in face.names() {
816 if matches!(
817 name.name_id,
818 ttf_parser::name_id::TYPOGRAPHIC_FAMILY | ttf_parser::name_id::FAMILY
819 ) && let Some(value) = name.to_string().filter(|value| !value.is_empty())
820 && !families
821 .iter()
822 .any(|existing: &String| existing.eq_ignore_ascii_case(&value))
823 {
824 families.push(value);
825 }
826 }
827 let weight = FontWeight::try_new(face.weight().to_number()).unwrap_or(FontWeight::NORMAL);
828 let style = if face.is_italic() {
829 FontStyle::Italic
830 } else {
831 FontStyle::Normal
832 };
833 let units_per_em = face.units_per_em() as f32;
834 let height = (face.ascender() as f32 - face.descender() as f32).abs();
835 let ab_glyph_scale_factor =
836 if units_per_em.is_finite() && units_per_em > 0.0 && height.is_finite() && height > 0.0 {
837 height / units_per_em
838 } else {
839 1.0
840 };
841
842 SoftwareTextFontMetadata {
843 families: Arc::from(families),
844 registered_family: None,
845 weight,
846 style,
847 ab_glyph_scale_factor,
848 tracking: FontTracking::from_face(&face),
849 }
850}
851
852type TextMetricsParams = (u32, u64, u64, usize);
855
856struct SoftwareTextMetricsCache {
857 map: PassAgedCache<TextCacheKey<TextMetricsParams>, TextMetrics>,
858 glyph_metrics: SoftwareTextGlyphMetricsCache,
859}
860
861impl SoftwareTextMetricsCache {
862 fn new(capacity: usize) -> Self {
863 Self {
864 map: PassAgedCache::with_capacity_at_least_one(capacity),
865 glyph_metrics: SoftwareTextGlyphMetricsCache::new(),
866 }
867 }
868
869 fn get_or_measure(
870 &mut self,
871 fonts: &SoftwareTextFontSet,
872 text: &AnnotatedString,
873 style: &TextStyle,
874 ) -> TextMetrics {
875 self.get_or_measure_range(fonts, text, 0..text.text.len(), style)
876 }
877
878 fn get_or_measure_range(
879 &mut self,
880 fonts: &SoftwareTextFontSet,
881 text: &AnnotatedString,
882 range: std::ops::Range<usize>,
883 style: &TextStyle,
884 ) -> TextMetrics {
885 let font_size = resolve_font_size(style);
886 let probe = TextProbe::new(
887 &text.text[range.clone()],
888 (
889 font_size.to_bits(),
890 style.measurement_hash(),
891 text.span_measurement_hash(),
892 range.start,
893 ),
894 );
895 if let Some(metrics) = self.map.get(probe.key()).copied() {
896 return metrics;
897 }
898
899 let metrics = if range == (0..text.text.len()) {
900 measure_annotated_text_with_font_set_cached(text, style, font_size, fonts, self)
901 } else {
902 let layout = AnnotatedTextLayout::new_range(
903 text.into(),
904 range.clone(),
905 style,
906 font_size,
907 1.0,
908 measure_grid(),
909 fonts,
910 );
911 layout.map_or_else(
912 || fallback_text_metrics(&text.text[range], style, font_size),
913 |layout| measure_resolved_text(&layout, Some(self)),
914 )
915 };
916 self.map.push(probe.to_owned_key(), metrics);
917 metrics
918 }
919
920 fn begin_layout_pass(&mut self) {
922 self.map.begin_pass(|_| {});
923 }
924}
925
926fn measures_line_prefixes(
930 text: &AnnotatedString,
931 line_range: &std::ops::Range<usize>,
932 style: &TextStyle,
933) -> bool {
934 !requires_font_layout(&text.text, style)
935 && style_allows_prefix_widths(style)
936 && is_single_line_range(text, line_range)
937}
938
939fn is_single_line_range(text: &AnnotatedString, line_range: &std::ops::Range<usize>) -> bool {
940 line_range.start <= line_range.end
941 && line_range.end <= text.text.len()
942 && text.text.is_char_boundary(line_range.start)
943 && text.text.is_char_boundary(line_range.end)
944 && !text.text[line_range.clone()].contains('\n')
945}
946
947pub(crate) fn requires_font_layout(text: &str, style: &TextStyle) -> bool {
948 #[cfg(feature = "text-shaping")]
949 {
950 crate::text_shaping::required(text, style)
951 }
952 #[cfg(not(feature = "text-shaping"))]
953 {
954 let _ = (text, style);
955 false
956 }
957}
958
959#[derive(Clone, Copy, Debug)]
960struct CachedGlyphMetrics {
961 glyph_id: GlyphId,
962 advance_unscaled: f32,
963}
964
965#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
966struct GlyphMetricsKey {
967 font_hash: u64,
968 ch: char,
969}
970
971#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
972struct KernMetricsKey {
973 font_hash: u64,
974 previous_id: u32,
975 glyph_id: u32,
976}
977
978#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
979struct SoftwareTextGlyphMetricsStats {
980 glyph_hits: u64,
981 glyph_misses: u64,
982 kern_hits: u64,
983 kern_misses: u64,
984}
985
986struct SoftwareTextGlyphMetricsCache {
987 glyphs: DirectMappedCache<GlyphMetricsKey, CachedGlyphMetrics>,
988 kerns: DirectMappedCache<KernMetricsKey, f32>,
989 stats: SoftwareTextGlyphMetricsStats,
990}
991
992impl SoftwareTextGlyphMetricsCache {
993 fn new() -> Self {
994 Self {
995 glyphs: DirectMappedCache::with_slots_log2(SOFTWARE_TEXT_GLYPH_METRICS_SLOTS_LOG2),
996 kerns: DirectMappedCache::with_slots_log2(SOFTWARE_TEXT_KERN_METRICS_SLOTS_LOG2),
997 stats: SoftwareTextGlyphMetricsStats::default(),
998 }
999 }
1000
1001 #[cfg(test)]
1002 fn stats(&self) -> SoftwareTextGlyphMetricsStats {
1003 self.stats
1004 }
1005
1006 fn glyph_metrics<F, S>(
1007 &mut self,
1008 font: &SoftwareTextFont,
1009 scaled_font: &S,
1010 ch: char,
1011 ) -> CachedGlyphMetrics
1012 where
1013 F: Font,
1014 S: ScaleFont<F>,
1015 {
1016 if let Some((glyph_id, advance)) = font.font.ascii_glyph(ch) {
1019 self.stats.glyph_hits = self.stats.glyph_hits.saturating_add(1);
1020 return CachedGlyphMetrics {
1021 glyph_id,
1022 advance_unscaled: advance.max(0.0),
1023 };
1024 }
1025 let key = GlyphMetricsKey {
1026 font_hash: font.glyph_map_hash,
1027 ch,
1028 };
1029 if let Some(metrics) = self.glyphs.get(&key) {
1030 self.stats.glyph_hits = self.stats.glyph_hits.saturating_add(1);
1031 return metrics;
1032 }
1033
1034 let glyph_id = scaled_font.font().glyph_id(ch);
1035 let metrics = CachedGlyphMetrics {
1036 glyph_id,
1037 advance_unscaled: scaled_font.font().h_advance_unscaled(glyph_id).max(0.0),
1038 };
1039 self.glyphs.insert(key, metrics);
1040 self.stats.glyph_misses = self.stats.glyph_misses.saturating_add(1);
1041 metrics
1042 }
1043
1044 fn kern<F, S>(
1045 &mut self,
1046 font: &SoftwareTextFont,
1047 scaled_font: &S,
1048 previous: (char, GlyphId),
1049 next: (char, GlyphId),
1050 ) -> f32
1051 where
1052 F: Font,
1053 S: ScaleFont<F>,
1054 {
1055 if let Some(kern) = font.font.ascii_kern(previous, next) {
1056 self.stats.kern_hits = self.stats.kern_hits.saturating_add(1);
1057 return kern;
1058 }
1059 let ((_, previous_id), (_, glyph_id)) = (previous, next);
1060 let key = KernMetricsKey {
1061 font_hash: font.content_hash(),
1062 previous_id: previous_id.0.into(),
1063 glyph_id: glyph_id.0.into(),
1064 };
1065 if let Some(kern) = self.kerns.get(&key) {
1066 self.stats.kern_hits = self.stats.kern_hits.saturating_add(1);
1067 return kern;
1068 }
1069
1070 let kern = scaled_font.font().kern_unscaled(previous_id, glyph_id);
1071 self.kerns.insert(key, kern);
1072 self.stats.kern_misses = self.stats.kern_misses.saturating_add(1);
1073 kern
1074 }
1075}
1076
1077pub struct SoftwareTextMeasurer {
1078 fonts: SoftwareTextFontSet,
1079 cache: Mutex<SoftwareTextMetricsCache>,
1080 hyphenation: HyphenationDictionaryStore,
1081}
1082
1083impl SoftwareTextMeasurer {
1084 pub fn new(font: SoftwareTextFont, cache_capacity: usize) -> Self {
1085 Self::from_font_set(SoftwareTextFontSet::from_font(font), cache_capacity)
1086 }
1087
1088 pub fn from_font_set(fonts: SoftwareTextFontSet, cache_capacity: usize) -> Self {
1089 Self {
1090 fonts,
1091 cache: Mutex::new(SoftwareTextMetricsCache::new(cache_capacity)),
1092 hyphenation: HyphenationDictionaryStore::new(),
1093 }
1094 }
1095
1096 pub fn from_fonts_or_default(fonts: &[&'static [u8]], cache_capacity: usize) -> Self {
1097 Self::from_font_set(
1098 software_text_font_set_from_fonts_or_default(fonts),
1099 cache_capacity,
1100 )
1101 }
1102
1103 fn lock_cache(&self) -> MutexGuard<'_, SoftwareTextMetricsCache> {
1104 self.cache.lock().unwrap_or_else(PoisonError::into_inner)
1105 }
1106
1107 #[cfg(feature = "text-hyphenation")]
1108 pub fn register_hyphenation_dictionary_path(
1109 &self,
1110 locale: &str,
1111 path: impl AsRef<std::path::Path>,
1112 ) -> Result<(), HyphenationDictionaryError> {
1113 self.hyphenation.register_dictionary_path(locale, path)
1114 }
1115
1116 #[cfg(feature = "text-hyphenation")]
1117 pub fn register_hyphenation_dictionary_reader(
1118 &self,
1119 locale: &str,
1120 reader: &mut impl std::io::Read,
1121 ) -> Result<(), HyphenationDictionaryError> {
1122 self.hyphenation.register_dictionary_reader(locale, reader)
1123 }
1124}
1125
1126impl TextMeasurer for SoftwareTextMeasurer {
1127 fn measure(&self, text: &cranpose_ui::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
1128 self.lock_cache().get_or_measure(&self.fonts, text, style)
1129 }
1130
1131 fn begin_layout_pass(&self) {
1132 self.lock_cache().begin_layout_pass();
1133 }
1134
1135 fn measure_subsequence(
1136 &self,
1137 text: &cranpose_ui::text::AnnotatedString,
1138 range: std::ops::Range<usize>,
1139 style: &TextStyle,
1140 ) -> TextMetrics {
1141 let start = range.start.min(text.text.len());
1142 let end = range.end.max(start).min(text.text.len());
1143 self.lock_cache()
1144 .get_or_measure_range(&self.fonts, text, start..end, style)
1145 }
1146
1147 fn measure_line_prefix_widths(
1148 &self,
1149 text: &cranpose_ui::text::AnnotatedString,
1150 line_range: std::ops::Range<usize>,
1151 style: &TextStyle,
1152 ) -> Option<TextLinePrefixWidths> {
1153 if !measures_line_prefixes(text, &line_range, style) {
1154 return None;
1155 }
1156 let char_count = text.text[line_range.clone()].chars().count();
1157 let mut sink = PrefixWidthVecs {
1158 prefix_widths: Vec::with_capacity(char_count + 1),
1159 separator_before: Vec::with_capacity(char_count),
1160 };
1161 sink.prefix_widths.push(0.0);
1162 let overhang = walk_line_prefix_widths(
1163 text,
1164 line_range,
1165 style,
1166 &self.fonts,
1167 &mut self.lock_cache().glyph_metrics,
1168 &mut sink,
1169 );
1170 TextLinePrefixWidths::from_parts(sink.prefix_widths, sink.separator_before, overhang)
1171 }
1172
1173 fn measure_line_width(
1174 &self,
1175 text: &cranpose_ui::text::AnnotatedString,
1176 line_range: std::ops::Range<usize>,
1177 style: &TextStyle,
1178 ) -> Option<f32> {
1179 if requires_font_layout(&text.text, style) && is_single_line_range(text, &line_range) {
1180 return Some(self.measure_subsequence(text, line_range, style).width);
1181 }
1182 if !measures_line_prefixes(text, &line_range, style) {
1183 return None;
1184 }
1185 let mut sink = LineWidthSink::default();
1186 let overhang = walk_line_prefix_widths(
1187 text,
1188 line_range,
1189 style,
1190 &self.fonts,
1191 &mut self.lock_cache().glyph_metrics,
1192 &mut sink,
1193 );
1194 sink.width(overhang)
1195 }
1196
1197 fn line_height(&self, text: &cranpose_ui::text::AnnotatedString, style: &TextStyle) -> f32 {
1198 let font_size = resolve_font_size(style);
1199 max_line_height_for_annotated_text_with_resolver(text, style, font_size, &self.fonts)
1200 }
1201
1202 fn glyph_line_box(&self, style: &TextStyle) -> Option<(f32, f32)> {
1203 Some(font_glyph_line_box(style, self.fonts.resolve(style)?))
1204 }
1205
1206 fn first_baseline(&self, style: &TextStyle) -> Option<f32> {
1207 Some(self.line_box(style)?.baseline)
1208 }
1209
1210 fn line_box(&self, style: &TextStyle) -> Option<cranpose_ui::text::LineBox> {
1211 let font = self.fonts.resolve(style)?;
1212 Some(font_line_box(style, font, resolve_font_size(style)))
1213 }
1214
1215 fn line_box_and_height(
1216 &self,
1217 _node_id: Option<cranpose_core::NodeId>,
1218 text: &AnnotatedString,
1219 style: &TextStyle,
1220 ) -> (Option<cranpose_ui::text::LineBox>, f32) {
1221 let Some((font, covers)) = self.fonts.resolve_covering(&text.text, style) else {
1222 return (None, self.line_height(text, style));
1223 };
1224 let line_box = font_line_box(style, font, resolve_font_size(style));
1225 let height = if covers && text.span_styles.is_empty() {
1226 line_box.height
1227 } else {
1228 self.line_height(text, style)
1229 };
1230 (Some(line_box), height)
1231 }
1232
1233 fn visit_line_boxes(
1234 &self,
1235 text: &AnnotatedString,
1236 style: &TextStyle,
1237 visit: &mut dyn FnMut(cranpose_ui::text::LineBox),
1238 ) -> Option<()> {
1239 visit_annotated_line_boxes(text, style, &self.fonts, visit)
1240 }
1241
1242 fn get_offset_for_position(
1243 &self,
1244 text: &cranpose_ui::text::AnnotatedString,
1245 style: &TextStyle,
1246 x: f32,
1247 y: f32,
1248 ) -> usize {
1249 annotated_offset_for_position(text, style, x, y, &self.fonts)
1250 }
1251
1252 fn get_cursor_x_for_offset(
1253 &self,
1254 text: &cranpose_ui::text::AnnotatedString,
1255 style: &TextStyle,
1256 offset: usize,
1257 ) -> f32 {
1258 annotated_cursor_x_for_offset(text, style, offset, &self.fonts)
1259 }
1260
1261 fn layout(
1262 &self,
1263 text: &cranpose_ui::text::AnnotatedString,
1264 style: &TextStyle,
1265 ) -> TextLayoutResult {
1266 layout_annotated_text_with_font_set(text, style, &self.fonts)
1267 }
1268
1269 fn choose_auto_hyphen_break(
1270 &self,
1271 line: &str,
1272 style: &TextStyle,
1273 segment_start_char: usize,
1274 measured_break_char: usize,
1275 ) -> Option<usize> {
1276 self.hyphenation.choose_auto_hyphen_break(
1277 line,
1278 style,
1279 segment_start_char,
1280 measured_break_char,
1281 )
1282 }
1283}
1284
1285#[derive(Clone, Copy)]
1286enum GlyphRasterStyle {
1287 Fill,
1288 Stroke { width_px: f32 },
1289}
1290
1291#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1292pub struct SoftwareGlyphAtlasKey {
1293 pub font_hash: u64,
1294 pub glyph_id: u32,
1295 pub scale_x_bits: u32,
1296 pub scale_y_bits: u32,
1297 pub embolden_px_bits: u32,
1298 pub slant_bits: u32,
1299}
1300
1301#[derive(Clone)]
1302pub struct SoftwareGlyphAtlasMask {
1303 pub alpha: Arc<[f32]>,
1304 pub width: usize,
1305 pub height: usize,
1306}
1307
1308#[derive(Clone)]
1309pub struct SoftwareGlyphAtlasGlyph {
1310 pub key: SoftwareGlyphAtlasKey,
1311 pub mask: SoftwareGlyphAtlasMask,
1312 pub x: i32,
1313 pub y: i32,
1314 pub color: Color,
1315}
1316
1317#[derive(Clone, Copy)]
1318pub struct SoftwareGlyphAtlasPlacement {
1319 pub key: SoftwareGlyphAtlasKey,
1320 pub x: i32,
1321 pub y: i32,
1322 pub width: usize,
1323 pub height: usize,
1324 pub color: Color,
1325}
1326
1327#[derive(Clone)]
1328pub enum SoftwareGlyphAtlasRunGlyph {
1329 Cached(SoftwareGlyphAtlasPlacement),
1330 New(SoftwareGlyphAtlasGlyph),
1331}
1332
1333impl SoftwareGlyphAtlasRunGlyph {
1334 pub fn placement(&self) -> SoftwareGlyphAtlasPlacement {
1335 match self {
1336 Self::Cached(placement) => *placement,
1337 Self::New(glyph) => SoftwareGlyphAtlasPlacement {
1338 key: glyph.key,
1339 x: glyph.x,
1340 y: glyph.y,
1341 width: glyph.mask.width,
1342 height: glyph.mask.height,
1343 color: glyph.color,
1344 },
1345 }
1346 }
1347}
1348
1349#[derive(Clone)]
1350struct GlyphMask {
1351 alpha: Arc<[f32]>,
1352 width: usize,
1353 height: usize,
1354 origin_x: i32,
1355 origin_y: i32,
1356}
1357
1358#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1359pub struct SoftwareGlyphRasterCacheStats {
1360 pub entries: usize,
1361 pub hits: u64,
1362 pub misses: u64,
1363}
1364
1365struct SegmentGlyphMetrics {
1370 slots: [u8; ASCII_COUNT],
1372 metrics: Vec<CachedAtlasGlyphMetrics>,
1373}
1374
1375impl SegmentGlyphMetrics {
1376 fn new() -> Self {
1377 Self {
1378 slots: [0; ASCII_COUNT],
1379 metrics: Vec::new(),
1380 }
1381 }
1382
1383 fn clear(&mut self) {
1384 self.slots.fill(0);
1385 self.metrics.clear();
1386 }
1387
1388 fn get(&self, ch: char) -> Option<CachedAtlasGlyphMetrics> {
1389 let index = usize::from(self.slots[ascii_slot(ch)?]).checked_sub(1)?;
1390 self.metrics.get(index).copied()
1391 }
1392
1393 fn insert(&mut self, ch: char, metrics: CachedAtlasGlyphMetrics) {
1394 let Some(slot) = ascii_slot(ch) else {
1395 return;
1396 };
1397 let Ok(index) = u8::try_from(self.metrics.len() + 1) else {
1398 return;
1399 };
1400 self.slots[slot] = index;
1401 self.metrics.push(metrics);
1402 }
1403}
1404
1405#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1406enum GlyphRasterStyleKey {
1407 Fill,
1408 Stroke { width_px_bits: u32 },
1409}
1410
1411impl GlyphRasterStyleKey {
1412 fn from_style(style: GlyphRasterStyle) -> Self {
1413 match style {
1414 GlyphRasterStyle::Fill => Self::Fill,
1415 GlyphRasterStyle::Stroke { width_px } => Self::Stroke {
1416 width_px_bits: width_px.to_bits(),
1417 },
1418 }
1419 }
1420}
1421
1422#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1423struct GlyphMaskCacheKey {
1424 font_hash: u64,
1425 glyph_id: u32,
1426 scale_x_bits: u32,
1427 scale_y_bits: u32,
1428 raster_style: GlyphRasterStyleKey,
1429 embolden_px_bits: u32,
1430 slant_bits: u32,
1431}
1432
1433#[derive(Clone)]
1434struct CachedGlyphMask {
1435 alpha: Arc<[f32]>,
1436 width: usize,
1437 height: usize,
1438 origin_offset_x: i32,
1439 origin_offset_y: i32,
1440}
1441
1442impl CachedGlyphMask {
1443 fn from_mask(mask: GlyphMask, glyph: &Glyph) -> Self {
1444 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1445 Self {
1446 alpha: mask.alpha,
1447 width: mask.width,
1448 height: mask.height,
1449 origin_offset_x: mask.origin_x - glyph_x,
1450 origin_offset_y: mask.origin_y - glyph_y,
1451 }
1452 }
1453
1454 fn instantiate(&self, glyph: &Glyph) -> GlyphMask {
1455 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1456 GlyphMask {
1457 alpha: Arc::clone(&self.alpha),
1458 width: self.width,
1459 height: self.height,
1460 origin_x: glyph_x + self.origin_offset_x,
1461 origin_y: glyph_y + self.origin_offset_y,
1462 }
1463 }
1464
1465 fn placement(&self, glyph: &Glyph) -> (i32, i32, usize, usize) {
1466 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1467 (
1468 glyph_x + self.origin_offset_x,
1469 glyph_y + self.origin_offset_y,
1470 self.width,
1471 self.height,
1472 )
1473 }
1474
1475 fn atlas_metrics(&self, key: SoftwareGlyphAtlasKey) -> CachedAtlasGlyphMetrics {
1476 CachedAtlasGlyphMetrics {
1477 key,
1478 width: self.width,
1479 height: self.height,
1480 origin_offset_x: self.origin_offset_x,
1481 origin_offset_y: self.origin_offset_y,
1482 }
1483 }
1484}
1485
1486#[derive(Clone, Copy)]
1487struct CachedAtlasGlyphMetrics {
1488 key: SoftwareGlyphAtlasKey,
1489 width: usize,
1490 height: usize,
1491 origin_offset_x: i32,
1492 origin_offset_y: i32,
1493}
1494
1495impl CachedAtlasGlyphMetrics {
1496 fn placement(self, glyph: &Glyph, color: Color) -> SoftwareGlyphAtlasPlacement {
1497 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1498 SoftwareGlyphAtlasPlacement {
1499 key: self.key,
1500 x: glyph_x + self.origin_offset_x,
1501 y: glyph_y + self.origin_offset_y,
1502 width: self.width,
1503 height: self.height,
1504 color,
1505 }
1506 }
1507}
1508
1509pub struct SoftwareGlyphRasterCache {
1510 masks: BoundedLruCache<GlyphMaskCacheKey, CachedGlyphMask>,
1511 kerns: DirectMappedCache<KernMetricsKey, f32>,
1512 segment_metrics: SegmentGlyphMetrics,
1513 hits: u64,
1514 misses: u64,
1515}
1516
1517impl SoftwareGlyphRasterCache {
1518 pub fn with_capacity_at_least_one(capacity: usize) -> Self {
1519 Self {
1520 masks: BoundedLruCache::with_capacity_at_least_one(capacity),
1521 kerns: DirectMappedCache::with_slots_log2(SOFTWARE_TEXT_KERN_METRICS_SLOTS_LOG2),
1522 segment_metrics: SegmentGlyphMetrics::new(),
1523 hits: 0,
1524 misses: 0,
1525 }
1526 }
1527
1528 fn kern_unscaled(
1532 &mut self,
1533 font_hash: u64,
1534 font: &impl Font,
1535 previous: GlyphId,
1536 glyph: GlyphId,
1537 ) -> f32 {
1538 let key = KernMetricsKey {
1539 font_hash,
1540 previous_id: previous.0.into(),
1541 glyph_id: glyph.0.into(),
1542 };
1543 if let Some(kern) = self.kerns.get(&key) {
1544 return kern;
1545 }
1546 let kern = font.kern_unscaled(previous, glyph);
1547 self.kerns.insert(key, kern);
1548 kern
1549 }
1550
1551 pub fn stats(&self) -> SoftwareGlyphRasterCacheStats {
1552 SoftwareGlyphRasterCacheStats {
1553 entries: self.masks.len(),
1554 hits: self.hits,
1555 misses: self.misses,
1556 }
1557 }
1558
1559 fn get(&mut self, key: &GlyphMaskCacheKey, glyph: &Glyph) -> Option<GlyphMask> {
1560 let mask = self.masks.get(key)?.instantiate(glyph);
1561 self.hits = self.hits.saturating_add(1);
1562 Some(mask)
1563 }
1564
1565 fn get_atlas_placement(
1566 &mut self,
1567 key: &GlyphMaskCacheKey,
1568 glyph: &Glyph,
1569 ) -> Option<(SoftwareGlyphAtlasKey, i32, i32, usize, usize)> {
1570 let atlas_key = glyph_atlas_key_from_mask_key(*key)?;
1571 let (x, y, width, height) = self.masks.get(key)?.placement(glyph);
1572 self.hits = self.hits.saturating_add(1);
1573 Some((atlas_key, x, y, width, height))
1574 }
1575
1576 fn get_atlas_metrics(&mut self, key: &GlyphMaskCacheKey) -> Option<CachedAtlasGlyphMetrics> {
1577 let atlas_key = glyph_atlas_key_from_mask_key(*key)?;
1578 let metrics = self.masks.get(key)?.atlas_metrics(atlas_key);
1579 self.hits = self.hits.saturating_add(1);
1580 Some(metrics)
1581 }
1582
1583 pub fn atlas_glyph_for_placement(
1584 &mut self,
1585 placement: &SoftwareGlyphAtlasPlacement,
1586 ) -> Option<SoftwareGlyphAtlasGlyph> {
1587 let key = GlyphMaskCacheKey {
1588 font_hash: placement.key.font_hash,
1589 glyph_id: placement.key.glyph_id,
1590 scale_x_bits: placement.key.scale_x_bits,
1591 scale_y_bits: placement.key.scale_y_bits,
1592 raster_style: GlyphRasterStyleKey::Fill,
1593 embolden_px_bits: placement.key.embolden_px_bits,
1594 slant_bits: placement.key.slant_bits,
1595 };
1596 let mask = self.masks.get(&key)?;
1597 self.hits = self.hits.saturating_add(1);
1598 Some(SoftwareGlyphAtlasGlyph {
1599 key: placement.key,
1600 mask: SoftwareGlyphAtlasMask {
1601 alpha: Arc::clone(&mask.alpha),
1602 width: mask.width,
1603 height: mask.height,
1604 },
1605 x: placement.x,
1606 y: placement.y,
1607 color: placement.color,
1608 })
1609 }
1610
1611 fn put(&mut self, key: GlyphMaskCacheKey, glyph: &Glyph, mask: GlyphMask) -> GlyphMask {
1612 let cached = CachedGlyphMask::from_mask(mask, glyph);
1613 let mask = cached.instantiate(glyph);
1614 self.masks.put(key, cached);
1615 self.misses = self.misses.saturating_add(1);
1616 mask
1617 }
1618}
1619
1620struct RasterFontRef<'a, F> {
1621 font: &'a F,
1622 ab_glyph_scale_factor: f32,
1623 weight: FontWeight,
1624 style: FontStyle,
1625 tracking: &'a FontTracking,
1626}
1627
1628#[derive(Clone, Copy)]
1629struct TextWeightSynthesis {
1630 embolden_px: f32,
1631 advance_scale: f32,
1632}
1633
1634impl TextWeightSynthesis {
1635 fn none() -> Self {
1636 Self {
1637 embolden_px: 0.0,
1638 advance_scale: 1.0,
1639 }
1640 }
1641
1642 const FAKE_BOLD_MIN_WEIGHT: u16 = 600;
1643 const FAKE_BOLD_MIN_DELTA: u16 = 200;
1644
1645 fn for_style(
1646 style: &TextStyle,
1647 resolved_weight: FontWeight,
1648 font_size: f32,
1649 scale: f32,
1650 ) -> Self {
1651 let requested_weight = style.span_style.font_weight.unwrap_or_default();
1652 if requested_weight <= resolved_weight {
1653 return Self::none();
1654 }
1655 if requested_weight.value() < Self::FAKE_BOLD_MIN_WEIGHT
1656 || requested_weight.value() - resolved_weight.value() < Self::FAKE_BOLD_MIN_DELTA
1657 {
1658 return Self::none();
1659 }
1660
1661 let synthesis = style
1662 .span_style
1663 .font_synthesis
1664 .unwrap_or(FontSynthesis::All);
1665 if !matches!(synthesis, FontSynthesis::All | FontSynthesis::Weight) {
1666 return Self::none();
1667 }
1668
1669 let weight_delta = (requested_weight.value() - resolved_weight.value()) as f32;
1670 let strength = (weight_delta / 300.0).clamp(0.0, 1.5);
1671 Self {
1672 embolden_px: (font_size * scale * 0.055 * strength).clamp(0.0, 3.0 * scale),
1673 advance_scale: 1.0 + 0.085 * strength.min(1.0),
1674 }
1675 }
1676
1677 fn apply_width(self, width: f32) -> f32 {
1678 width * self.advance_scale
1679 }
1680}
1681
1682#[derive(Clone, Copy)]
1683struct TextStyleSynthesis {
1684 slant: f32,
1685 font_size: f32,
1686 scale: f32,
1687}
1688
1689impl TextStyleSynthesis {
1690 fn none() -> Self {
1691 Self {
1692 slant: 0.0,
1693 font_size: 0.0,
1694 scale: 1.0,
1695 }
1696 }
1697
1698 fn for_style(style: &TextStyle, resolved_style: FontStyle, font_size: f32, scale: f32) -> Self {
1699 let requested_style = style.span_style.font_style.unwrap_or_default();
1700 if requested_style != FontStyle::Italic || resolved_style == FontStyle::Italic {
1701 return Self::none();
1702 }
1703
1704 let synthesis = style
1705 .span_style
1706 .font_synthesis
1707 .unwrap_or(FontSynthesis::All);
1708 if !matches!(synthesis, FontSynthesis::All | FontSynthesis::Style) {
1709 return Self::none();
1710 }
1711
1712 Self {
1713 slant: 0.22,
1714 font_size,
1715 scale,
1716 }
1717 }
1718
1719 fn visual_overhang_px(self) -> f32 {
1720 if self.slant <= 0.0 || !self.font_size.is_finite() || !self.scale.is_finite() {
1721 return 0.0;
1722 }
1723 (self.font_size * self.scale * self.slant).ceil().max(0.0)
1724 }
1725}
1726
1727pub fn rasterize_text_to_image(
1728 text: &str,
1729 rect: Rect,
1730 style: &TextStyle,
1731 fallback_color: Color,
1732 font_size: f32,
1733 scale: f32,
1734 font: &SoftwareTextFont,
1735) -> Option<ImageBitmap> {
1736 if requires_font_layout(text, style) {
1737 return rasterize_styled_text_region(
1738 StyledTextRef {
1739 text,
1740 span_styles: &[],
1741 },
1742 rect,
1743 Point::new(rect.x, rect.y),
1744 style,
1745 fallback_color,
1746 font_size,
1747 scale,
1748 font.into(),
1749 None,
1750 );
1751 }
1752 let shaped = font.shaped_for(style);
1753 let font = &*shaped;
1754 rasterize_text_to_image_impl(
1755 TextRasterImageRequest {
1756 text,
1757 rect,
1758 style,
1759 fallback_color,
1760 font_size,
1761 scale,
1762 },
1763 font.raster_ref(),
1764 font.content_hash(),
1765 None,
1766 )
1767}
1768
1769#[expect(clippy::too_many_arguments)]
1770pub fn rasterize_text_to_image_with_glyph_cache(
1771 text: &str,
1772 rect: Rect,
1773 style: &TextStyle,
1774 fallback_color: Color,
1775 font_size: f32,
1776 scale: f32,
1777 font: &SoftwareTextFont,
1778 glyph_cache: &mut SoftwareGlyphRasterCache,
1779) -> Option<ImageBitmap> {
1780 if requires_font_layout(text, style) {
1781 return rasterize_styled_text_region(
1782 StyledTextRef {
1783 text,
1784 span_styles: &[],
1785 },
1786 rect,
1787 Point::new(rect.x, rect.y),
1788 style,
1789 fallback_color,
1790 font_size,
1791 scale,
1792 font.into(),
1793 Some(glyph_cache),
1794 );
1795 }
1796 let shaped = font.shaped_for(style);
1797 let font = &*shaped;
1798 rasterize_text_to_image_impl(
1799 TextRasterImageRequest {
1800 text,
1801 rect,
1802 style,
1803 fallback_color,
1804 font_size,
1805 scale,
1806 },
1807 font.raster_ref(),
1808 font.content_hash(),
1809 Some(glyph_cache),
1810 )
1811}
1812
1813#[derive(Clone, Copy)]
1818pub struct StyledTextRef<'a> {
1819 pub text: &'a str,
1820 pub span_styles: &'a [RangeStyle<SpanStyle>],
1821}
1822
1823impl StyledTextRef<'_> {
1824 fn is_empty(&self) -> bool {
1825 self.text.is_empty()
1826 }
1827
1828 pub(crate) fn span_boundaries(&self) -> cranpose_ui::text::SpanBoundaries {
1829 let mut boundaries: cranpose_ui::text::SpanBoundaries =
1830 smallvec::smallvec![0, self.text.len()];
1831 for span in self.span_styles {
1832 boundaries.push(span.range.start);
1833 boundaries.push(span.range.end);
1834 }
1835 boundaries.sort_unstable();
1836 boundaries.dedup();
1837 boundaries.retain(|b| *b <= self.text.len() && self.text.is_char_boundary(*b));
1838 boundaries
1839 }
1840}
1841
1842impl<'a> From<&'a AnnotatedString> for StyledTextRef<'a> {
1843 fn from(text: &'a AnnotatedString) -> Self {
1844 Self {
1845 text: text.text.as_str(),
1846 span_styles: &text.span_styles,
1847 }
1848 }
1849}
1850
1851impl<'a> From<&'a RenderString> for StyledTextRef<'a> {
1852 fn from(text: &'a RenderString) -> Self {
1853 Self {
1854 text: text.text(),
1855 span_styles: text.span_styles(),
1856 }
1857 }
1858}
1859
1860fn annotated_line_alignment_offsets(
1861 layout: &AnnotatedTextLayout<'_>,
1862 text: &StyledTextRef<'_>,
1863 style: &TextStyle,
1864 scale: f32,
1865) -> Option<Vec<f32>> {
1866 if !text.text.contains('\n') {
1867 return None;
1868 }
1869 let align_fraction = cranpose_ui::text::text_align_fraction(style, text.text);
1870 if align_fraction == 0.0 {
1871 return None;
1872 }
1873 let mut offsets = Vec::with_capacity(layout.lines.len());
1874 let mut block = 0.0_f32;
1875 for index in 0..layout.lines.len() {
1876 let advance = layout.walk_line(index, 0.0, |segment| {
1877 Some(annotated_raster_segment_advance(&segment, scale))
1878 })?;
1879 block = block.max(advance);
1880 offsets.push(advance);
1881 }
1882 for offset in &mut offsets {
1883 *offset = ((block - *offset) * align_fraction).max(0.0);
1884 }
1885 Some(offsets)
1886}
1887
1888fn segment_advance_px(
1889 font: &impl Font,
1890 content: &str,
1891 font_px_size: f32,
1892 letter_spacing: f32,
1893) -> f32 {
1894 let scaled_font = font.as_scaled(PxScale::from(font_px_size));
1895 let mut caret = 0.0f32;
1896 let mut previous = None;
1897 for ch in content.chars() {
1898 let glyph_id = scaled_font.glyph_id(ch);
1899 if let Some(previous_id) = previous {
1900 caret += scaled_font.kern(previous_id, glyph_id);
1901 }
1902 caret += letter_spacing + scaled_font.h_advance(glyph_id);
1903 previous = Some(glyph_id);
1904 }
1905 caret.max(0.0)
1906}
1907
1908fn annotated_raster_segment_advance(segment: &AnnotatedTextSegment<'_>, scale: f32) -> f32 {
1909 #[cfg(feature = "text-shaping")]
1910 if requires_font_layout(segment.text, segment.style)
1911 && let Some(run) = segment.font.shaped_run(segment.text, segment.style)
1912 {
1913 return shaped_run_width(&run, segment.font, segment.style, segment.font_size, scale);
1914 }
1915 let font = segment.font.shaped_for(segment.style);
1916 segment_advance_px(
1917 &font.font,
1918 segment.text,
1919 font.ab_glyph_px_size(segment.font_size) * scale,
1920 font.metadata
1921 .tracking
1922 .resolve(segment.style, segment.font_size)
1923 * scale,
1924 )
1925}
1926
1927fn text_render_request_is_degenerate(
1928 text_is_empty: bool,
1929 rect: Rect,
1930 font_size: f32,
1931 scale: f32,
1932) -> bool {
1933 text_is_empty
1934 || rect.width <= 0.0
1935 || rect.height <= 0.0
1936 || !font_size.is_finite()
1937 || font_size <= 0.0
1938 || !scale.is_finite()
1939 || scale <= 0.0
1940}
1941
1942#[derive(Clone, Copy)]
1943struct TextSegmentMetrics {
1944 font_px_size: f32,
1945 letter_spacing: f32,
1946 align_fraction: f32,
1947 weight_synthesis: TextWeightSynthesis,
1948 style_synthesis: TextStyleSynthesis,
1949 line_height: f32,
1950 first_baseline_y: f32,
1951}
1952
1953fn text_segment_metrics(
1954 text: &str,
1955 local_rect: Rect,
1956 style: &TextStyle,
1957 font_size: f32,
1958 scale: f32,
1959 font: &SoftwareTextFont,
1960) -> TextSegmentMetrics {
1961 let font_px_size = font.ab_glyph_px_size(font_size) * scale;
1962 let letter_spacing = font.metadata.tracking.resolve(style, font_size) * scale;
1963 let align_fraction = cranpose_ui::text::text_align_fraction(style, text);
1964 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, scale);
1965 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, scale);
1966 let metrics = vertical_metrics(&font.font, font_px_size);
1967 let line_box = line_box_for(style, metrics, asked_line_height(style, scale), 1.0);
1968 TextSegmentMetrics {
1969 font_px_size,
1970 letter_spacing,
1971 align_fraction,
1972 weight_synthesis,
1973 style_synthesis,
1974 line_height: line_box.height,
1975 first_baseline_y: local_rect.y + line_box.first_baseline(),
1976 }
1977}
1978
1979fn text_segment_supports_solid_atlas(style: &TextStyle) -> bool {
1980 style_can_atlas_solid_fill(style)
1981 && style
1982 .paragraph_style
1983 .text_motion
1984 .unwrap_or(TextMotion::Static)
1985 == TextMotion::Static
1986}
1987
1988fn style_can_rasterize_direct_solid(style: &TextStyle) -> bool {
1989 !style
1990 .span_style
1991 .shadow
1992 .is_some_and(|shadow| shadow.color.3 > 0.0)
1993 && matches!(
1994 style.span_style.brush.as_ref(),
1995 None | Some(Brush::Solid(_))
1996 )
1997}
1998
1999fn style_can_atlas_solid_fill(style: &TextStyle) -> bool {
2000 style_can_rasterize_direct_solid(style)
2001 && match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
2002 TextDrawStyle::Fill => true,
2003 TextDrawStyle::Stroke { width } => !width.is_finite() || width <= 0.0,
2004 }
2005}
2006
2007fn visit_annotated_text_segments<'a>(
2008 text: impl Into<StyledTextRef<'a>>,
2009 style: &TextStyle,
2010 font_size: f32,
2011 scale: f32,
2012 fonts: &'a SoftwareTextFontSet,
2013 visit: impl FnMut(AnnotatedTextSegment<'_>) -> Option<f32>,
2014) -> Option<TextMetrics> {
2015 let text = text.into();
2016 let layout = AnnotatedTextLayout::new(text, style, font_size, scale, 1.0, fonts)?;
2017 let offsets = annotated_line_alignment_offsets(&layout, &text, style, scale);
2018 layout.walk(offsets.as_deref(), visit)
2019}
2020
2021#[expect(clippy::too_many_arguments)]
2022pub fn rasterize_annotated_text_to_image_with_glyph_cache<'a>(
2023 text: impl Into<StyledTextRef<'a>>,
2024 rect: Rect,
2025 style: &TextStyle,
2026 fallback_color: Color,
2027 font_size: f32,
2028 scale: f32,
2029 fonts: &SoftwareTextFontSet,
2030 glyph_cache: &mut SoftwareGlyphRasterCache,
2031) -> Option<ImageBitmap> {
2032 let text = text.into();
2033 rasterize_annotated_text_region(
2034 text,
2035 rect,
2036 Point::new(rect.x, rect.y),
2037 style,
2038 fallback_color,
2039 font_size,
2040 scale,
2041 fonts,
2042 Some(glyph_cache),
2043 )
2044}
2045
2046#[expect(clippy::too_many_arguments)]
2050pub fn rasterize_annotated_text_region<'a>(
2051 text: impl Into<StyledTextRef<'a>>,
2052 rect: Rect,
2053 text_origin: Point,
2054 style: &TextStyle,
2055 fallback_color: Color,
2056 font_size: f32,
2057 scale: f32,
2058 fonts: &SoftwareTextFontSet,
2059 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
2060) -> Option<ImageBitmap> {
2061 rasterize_styled_text_region(
2062 text.into(),
2063 rect,
2064 text_origin,
2065 style,
2066 fallback_color,
2067 font_size,
2068 scale,
2069 fonts.into(),
2070 glyph_cache,
2071 )
2072}
2073
2074#[expect(clippy::too_many_arguments)]
2075fn rasterize_styled_text_region(
2076 text: StyledTextRef<'_>,
2077 rect: Rect,
2078 text_origin: Point,
2079 style: &TextStyle,
2080 fallback_color: Color,
2081 font_size: f32,
2082 scale: f32,
2083 fonts: FontResolver<'_>,
2084 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
2085) -> Option<ImageBitmap> {
2086 if text_render_request_is_degenerate(text.is_empty(), rect, font_size, scale) {
2087 return None;
2088 }
2089 if text.span_styles.is_empty()
2090 && !requires_font_layout(text.text, style)
2091 && text_origin.x == rect.x
2092 && text_origin.y == rect.y
2093 && let Some(font) = fonts.single_font_for_text(text.text, style)
2094 {
2095 return match glyph_cache.as_deref_mut() {
2096 Some(cache) => rasterize_text_to_image_with_glyph_cache(
2097 text.text,
2098 rect,
2099 style,
2100 fallback_color,
2101 font_size,
2102 scale,
2103 font,
2104 cache,
2105 ),
2106 None => rasterize_text_to_image(
2107 text.text,
2108 rect,
2109 style,
2110 fallback_color,
2111 font_size,
2112 scale,
2113 font,
2114 ),
2115 };
2116 }
2117 let layout = AnnotatedTextLayout::new(text, style, font_size, scale, 1.0, fonts)?;
2118 let solid = layout.all_styles(style_can_rasterize_direct_solid);
2119 let width = rect.width.ceil().max(1.0) as u32;
2120 let height = rect.height.ceil().max(1.0) as u32;
2121 let mut canvas = TextRasterCanvas::new(width, height, solid);
2122 let offsets = annotated_line_alignment_offsets(&layout, &text, style, scale);
2123 let brush_extents = layout.has_non_solid_brush().then(|| {
2124 layout.brush_extents(offsets.as_deref(), scale, |segment| {
2125 annotated_raster_segment_advance(segment, scale)
2126 })
2127 });
2128 layout.walk(offsets.as_deref(), |segment| {
2129 let static_text_motion = segment
2130 .style
2131 .paragraph_style
2132 .text_motion
2133 .unwrap_or(TextMotion::Static)
2134 == TextMotion::Static;
2135 let origin = Point::new(
2136 text_origin.x - rect.x + segment.origin.x,
2137 text_origin.y - rect.y + segment.origin.y,
2138 );
2139 let local_rect = Rect {
2140 x: if static_text_motion {
2141 origin.x.round()
2142 } else {
2143 origin.x + rect.x.fract()
2144 },
2145 y: if static_text_motion {
2146 origin.y
2147 } else {
2148 origin.y + rect.y.fract()
2149 },
2150 width: width as f32,
2151 height: height as f32,
2152 };
2153 let fallback = Brush::solid(segment.style.resolve_text_color(fallback_color));
2154 let (brush, alpha) =
2155 segment
2156 .style
2157 .span_style
2158 .brush
2159 .as_ref()
2160 .map_or((&fallback, 1.0), |brush| {
2161 (
2162 brush,
2163 segment
2164 .style
2165 .span_style
2166 .alpha
2167 .unwrap_or(1.0)
2168 .clamp(0.0, 1.0),
2169 )
2170 });
2171 let brush_rect = annotated_brush_rect(
2172 brush,
2173 brush_extents.as_deref().unwrap_or_default(),
2174 &segment,
2175 text_origin,
2176 rect,
2177 )?;
2178 let paint = GlyphPaint {
2179 brush,
2180 alpha,
2181 shadow: segment
2182 .style
2183 .span_style
2184 .shadow
2185 .filter(|shadow| shadow.color.3 > 0.0),
2186 brush_rect,
2187 canvas_origin: Point::new(rect.x, rect.y),
2188 scale,
2189 static_text_motion,
2190 };
2191 Some(visit_text_segment_masks(
2192 segment.text,
2193 local_rect,
2194 segment.style,
2195 segment.font_size,
2196 scale,
2197 segment.font,
2198 glyph_cache.as_deref_mut(),
2199 &GlyphRasterClip::new(width, height, &paint),
2200 |mask| canvas.paint(mask, &paint),
2201 ))
2202 })?;
2203 canvas.into_image()
2204}
2205
2206fn annotated_brush_rect(
2207 brush: &Brush,
2208 extents: &[AnnotatedBrushExtent],
2209 segment: &AnnotatedTextSegment<'_>,
2210 text_origin: Point,
2211 fallback: Rect,
2212) -> Option<Rect> {
2213 if matches!(brush, Brush::Solid(_)) {
2214 return Some(fallback);
2215 }
2216 let index = extents
2217 .binary_search_by_key(&(segment.line_index, segment.style_index), |extent| {
2218 (extent.line_index, extent.style_index)
2219 })
2220 .ok()?;
2221 Some(extents[index].rect.translate(text_origin.x, text_origin.y))
2222}
2223
2224#[expect(clippy::too_many_arguments)]
2225fn walk_solid_text_atlas_segments<'a, T>(
2226 text: impl Into<StyledTextRef<'a>>,
2227 rect: Rect,
2228 style: &TextStyle,
2229 fallback_color: Color,
2230 font_size: f32,
2231 scale: f32,
2232 fonts: &SoftwareTextFontSet,
2233 glyph_cache: &mut SoftwareGlyphRasterCache,
2234 out: &mut Vec<T>,
2235 mut collect_segment: impl FnMut(
2236 &str,
2237 Rect,
2238 &TextStyle,
2239 Color,
2240 f32,
2241 f32,
2242 &SoftwareTextFont,
2243 &mut SoftwareGlyphRasterCache,
2244 &mut Vec<T>,
2245 ) -> Option<f32>,
2246) -> Option<()> {
2247 let text = text.into();
2248 if text_render_request_is_degenerate(text.is_empty(), rect, font_size, scale) {
2249 return Some(());
2250 }
2251 if text.span_styles.is_empty() && !requires_font_layout(text.text, style) {
2252 if !text_segment_supports_solid_atlas(style) {
2253 return None;
2254 }
2255 if let Some(font) = fonts.single_font_for_text(text.text, style) {
2256 return collect_segment(
2257 text.text,
2258 Rect {
2259 x: 0.0,
2260 y: 0.0,
2261 width: rect.width,
2262 height: rect.height,
2263 },
2264 style,
2265 style.resolve_text_color(fallback_color),
2266 font_size,
2267 scale,
2268 font,
2269 glyph_cache,
2270 out,
2271 )
2272 .map(|_| ());
2273 }
2274 }
2275 let initial_len = out.len();
2276 let result = visit_annotated_text_segments(text, style, font_size, scale, fonts, |segment| {
2277 if !text_segment_supports_solid_atlas(segment.style) {
2278 return None;
2279 }
2280 collect_segment(
2281 segment.text,
2282 Rect {
2283 x: segment.origin.x.round(),
2284 y: segment.origin.y,
2285 width: rect.width,
2286 height: rect.height,
2287 },
2288 segment.style,
2289 segment.style.resolve_text_color(fallback_color),
2290 segment.font_size,
2291 scale,
2292 segment.font,
2293 glyph_cache,
2294 out,
2295 )
2296 });
2297 if result.is_none() {
2298 out.truncate(initial_len);
2299 }
2300 result.map(|_| ())
2301}
2302
2303#[expect(clippy::too_many_arguments)]
2304pub fn collect_solid_text_atlas_glyphs(
2305 text: &AnnotatedString,
2306 rect: Rect,
2307 style: &TextStyle,
2308 fallback_color: Color,
2309 font_size: f32,
2310 scale: f32,
2311 fonts: &SoftwareTextFontSet,
2312 glyph_cache: &mut SoftwareGlyphRasterCache,
2313 out: &mut Vec<SoftwareGlyphAtlasGlyph>,
2314) -> Option<()> {
2315 walk_solid_text_atlas_segments(
2316 text,
2317 rect,
2318 style,
2319 fallback_color,
2320 font_size,
2321 scale,
2322 fonts,
2323 glyph_cache,
2324 out,
2325 collect_text_segment_solid_atlas_glyphs,
2326 )
2327}
2328
2329#[expect(clippy::too_many_arguments)]
2330pub fn collect_cached_solid_text_atlas_placements(
2331 text: &AnnotatedString,
2332 rect: Rect,
2333 style: &TextStyle,
2334 fallback_color: Color,
2335 font_size: f32,
2336 scale: f32,
2337 fonts: &SoftwareTextFontSet,
2338 glyph_cache: &mut SoftwareGlyphRasterCache,
2339 out: &mut Vec<SoftwareGlyphAtlasPlacement>,
2340) -> Option<()> {
2341 walk_solid_text_atlas_segments(
2342 text,
2343 rect,
2344 style,
2345 fallback_color,
2346 font_size,
2347 scale,
2348 fonts,
2349 glyph_cache,
2350 out,
2351 collect_text_segment_cached_solid_atlas_placements,
2352 )
2353}
2354
2355#[expect(clippy::too_many_arguments)]
2358pub fn collect_solid_text_atlas_run<'a>(
2359 text: impl Into<StyledTextRef<'a>>,
2360 rect: Rect,
2361 style: &TextStyle,
2362 fallback_color: Color,
2363 font_size: f32,
2364 scale: f32,
2365 fonts: &SoftwareTextFontSet,
2366 glyph_cache: &mut SoftwareGlyphRasterCache,
2367 out: &mut Vec<SoftwareGlyphAtlasRunGlyph>,
2368) -> Option<()> {
2369 walk_solid_text_atlas_segments(
2370 text,
2371 rect,
2372 style,
2373 fallback_color,
2374 font_size,
2375 scale,
2376 fonts,
2377 glyph_cache,
2378 out,
2379 collect_text_segment_solid_atlas_run,
2380 )
2381}
2382
2383pub fn measure_text_with_font(
2384 text: &str,
2385 style: &TextStyle,
2386 font_size: f32,
2387 font: &SoftwareTextFont,
2388) -> TextMetrics {
2389 #[cfg(feature = "text-shaping")]
2390 if requires_font_layout(text, style) {
2391 return measure_annotated_text_with_resolver(
2392 StyledTextRef {
2393 text,
2394 span_styles: &[],
2395 },
2396 style,
2397 font_size,
2398 font,
2399 None,
2400 );
2401 }
2402 measure_text_impl(text, style, font_size, font.shaped_for(style).raster_ref())
2403}
2404
2405fn measure_text_with_font_cached(
2406 text: &str,
2407 style: &TextStyle,
2408 font_size: f32,
2409 font: &SoftwareTextFont,
2410 cache: &mut SoftwareTextMetricsCache,
2411) -> TextMetrics {
2412 #[cfg(feature = "text-shaping")]
2413 if requires_font_layout(text, style) {
2414 return measure_shaped_text(text, style, font_size, font);
2415 }
2416 measure_text_impl_cached(text, style, font_size, &font.shaped_for(style), cache)
2417}
2418
2419pub fn measure_annotated_text_with_font(
2420 text: &AnnotatedString,
2421 style: &TextStyle,
2422 font_size: f32,
2423 font: &SoftwareTextFont,
2424) -> TextMetrics {
2425 if text.span_styles.is_empty() && !requires_font_layout(&text.text, style) {
2426 return measure_text_with_font(text.text.as_str(), style, font_size, font);
2427 }
2428 measure_annotated_text_with_resolver(text, style, font_size, font, None)
2429}
2430
2431pub fn measure_annotated_text_with_font_set(
2432 text: &AnnotatedString,
2433 style: &TextStyle,
2434 font_size: f32,
2435 fonts: &SoftwareTextFontSet,
2436) -> TextMetrics {
2437 if text.span_styles.is_empty()
2438 && let Some(font) = fonts.single_font_for_text(text.text.as_str(), style)
2439 {
2440 return measure_text_with_font(text.text.as_str(), style, font_size, font);
2441 }
2442 measure_annotated_text_with_resolver(text, style, font_size, fonts, None)
2443}
2444
2445fn measure_annotated_text_with_font_set_cached(
2446 text: &AnnotatedString,
2447 style: &TextStyle,
2448 font_size: f32,
2449 fonts: &SoftwareTextFontSet,
2450 cache: &mut SoftwareTextMetricsCache,
2451) -> TextMetrics {
2452 if text.span_styles.is_empty()
2453 && let Some(font) = fonts.single_font_for_text(text.text.as_str(), style)
2454 {
2455 return measure_text_with_font_cached(text.text.as_str(), style, font_size, font, cache);
2456 }
2457 measure_annotated_text_with_resolver(text, style, font_size, fonts, Some(cache))
2458}
2459
2460pub fn text_offset_for_position_with_font(
2461 text: &str,
2462 style: &TextStyle,
2463 x: f32,
2464 y: f32,
2465 font: &SoftwareTextFont,
2466) -> usize {
2467 if requires_font_layout(text, style) {
2468 return annotated_offset_for_position(
2469 StyledTextRef {
2470 text,
2471 span_styles: &[],
2472 },
2473 style,
2474 x,
2475 y,
2476 font,
2477 );
2478 }
2479 if text.is_empty() {
2480 return 0;
2481 }
2482
2483 let shaped = font.shaped_for(style);
2484 let font = &*shaped;
2485 let font_size = resolve_font_size(style);
2486 let line_box = font_line_box(style, font, font_size);
2487 let line_height = line_box.height;
2488
2489 let line_index = ((y + line_box.trim_top) / line_height).floor().max(0.0) as usize;
2490 let lines: Vec<&str> = text.split('\n').collect();
2491 let target_line = line_index.min(lines.len().saturating_sub(1));
2492
2493 let mut line_start_byte = 0;
2494 for line in lines.iter().take(target_line) {
2495 line_start_byte += line.len() + 1;
2496 }
2497
2498 let line_text = lines.get(target_line).unwrap_or(&"");
2499 if line_text.is_empty() {
2500 return line_start_byte;
2501 }
2502
2503 let mut best_offset = 0;
2504 let mut best_distance = f32::INFINITY;
2505 let mut current_byte_offset = 0;
2506
2507 for c in line_text.chars() {
2508 let prefix = &line_text[..current_byte_offset];
2509 let glyph_x = measure_text_impl(prefix, style, font_size, font.raster_ref()).width;
2510
2511 let char_str = &line_text[current_byte_offset..current_byte_offset + c.len_utf8()];
2512 let char_width = measure_text_impl(char_str, style, font_size, font.raster_ref())
2513 .width
2514 .max(font_size * 0.5);
2515
2516 let left_dist = (x - glyph_x).abs();
2517 if left_dist < best_distance {
2518 best_distance = left_dist;
2519 best_offset = current_byte_offset;
2520 }
2521
2522 let right_x = glyph_x + char_width;
2523 let right_dist = (x - right_x).abs();
2524 if right_dist < best_distance {
2525 best_distance = right_dist;
2526 best_offset = current_byte_offset + c.len_utf8();
2527 }
2528
2529 current_byte_offset += c.len_utf8();
2530 }
2531
2532 let total_width = measure_text_impl(line_text, style, font_size, font.raster_ref()).width;
2533 let end_dist = (x - total_width).abs();
2534 if end_dist < best_distance {
2535 best_offset = line_text.len();
2536 }
2537
2538 line_start_byte + best_offset.min(line_text.len())
2539}
2540
2541pub fn cursor_x_for_offset_with_font(
2542 text: &str,
2543 style: &TextStyle,
2544 offset: usize,
2545 font: &SoftwareTextFont,
2546) -> f32 {
2547 if requires_font_layout(text, style) {
2548 return annotated_cursor_x_for_offset(
2549 StyledTextRef {
2550 text,
2551 span_styles: &[],
2552 },
2553 style,
2554 offset,
2555 font,
2556 );
2557 }
2558 let clamped_offset = clamp_to_char_boundary(text, offset.min(text.len()));
2559 if clamped_offset == 0 {
2560 return 0.0;
2561 }
2562
2563 let font_size = resolve_font_size(style);
2564 let shaped = font.shaped_for(style);
2565 let font = &*shaped;
2566 measure_text_impl(&text[..clamped_offset], style, font_size, font.raster_ref()).width
2567}
2568
2569pub fn layout_text_with_font(
2570 text: &str,
2571 style: &TextStyle,
2572 font: &SoftwareTextFont,
2573) -> TextLayoutResult {
2574 if requires_font_layout(text, style) {
2575 return layout_annotated_text_with_font_set(
2576 StyledTextRef {
2577 text,
2578 span_styles: &[],
2579 },
2580 style,
2581 font,
2582 );
2583 }
2584 let shaped = font.shaped_for(style);
2585 let font = &*shaped;
2586 let font_size = resolve_font_size(style);
2587 let glyph_font_size = font.ab_glyph_px_size(font_size);
2588 let resolved_weight = font.weight();
2589 let font_ref = font.raster_ref();
2590 let letter_spacing = font.metadata.tracking.resolve(style, font_size);
2591 let weight_synthesis = TextWeightSynthesis::for_style(style, resolved_weight, font_size, 1.0);
2592 let line_box = font_line_box(style, font, font_size);
2593 let line_height = line_box.height;
2594 let font = &font.font;
2595 let scaled_font = font.as_scaled(PxScale::from(glyph_font_size));
2596
2597 let mut glyph_x_positions = Vec::new();
2598 let mut char_to_byte = Vec::new();
2599 let mut glyph_layouts = Vec::new();
2600 let mut lines = Vec::new();
2601 let mut current_x = 0.0f32;
2602 let mut line_start = 0;
2603 let mut y = -line_box.trim_top;
2604
2605 let mut iter = text.char_indices().peekable();
2606 while let Some((byte_offset, c)) = iter.next() {
2607 glyph_x_positions.push(current_x);
2608 char_to_byte.push(byte_offset);
2609
2610 if c == '\n' {
2611 lines.push(LineLayout {
2612 start_offset: line_start,
2613 end_offset: byte_offset,
2614 y,
2615 height: line_height,
2616 });
2617 line_start = byte_offset + 1;
2618 y += line_height;
2619 current_x = 0.0;
2620 } else {
2621 let glyph_id = scaled_font.glyph_id(c);
2622 let glyph_width =
2623 weight_synthesis.apply_width(scaled_font.h_advance(glyph_id).max(0.0));
2624 let glyph_end = byte_offset + c.len_utf8();
2625 if glyph_end > byte_offset {
2626 glyph_layouts.push(GlyphLayout {
2627 line_index: lines.len(),
2628 start_offset: byte_offset,
2629 end_offset: glyph_end,
2630 x: current_x,
2631 y,
2632 width: glyph_width,
2633 height: line_height,
2634 });
2635 }
2636 current_x += glyph_width;
2637 if let Some((_, next)) = iter.peek()
2638 && *next != '\n'
2639 {
2640 current_x += letter_spacing;
2641 }
2642 }
2643 }
2644
2645 glyph_x_positions.push(current_x);
2646 char_to_byte.push(text.len());
2647
2648 lines.push(LineLayout {
2649 start_offset: line_start,
2650 end_offset: text.len(),
2651 y,
2652 height: line_height,
2653 });
2654
2655 let metrics = measure_text_impl(text, style, font_size, font_ref);
2656 TextLayoutResult::new(
2657 text,
2658 TextLayoutData {
2659 width: metrics.width,
2660 height: metrics.height,
2661 line_height,
2662 glyph_x_positions,
2663 char_to_byte,
2664 lines,
2665 glyph_layouts,
2666 },
2667 )
2668}
2669
2670fn walk_annotated_glyphs(
2671 segment: &AnnotatedTextSegment<'_>,
2672 mut visit: impl FnMut(GlyphLayout),
2673) -> f32 {
2674 #[cfg(feature = "text-shaping")]
2675 if requires_font_layout(segment.text, segment.style)
2676 && let Some(run) = segment.font.shaped_run(segment.text, segment.style)
2677 {
2678 let font = segment.font;
2679 let weight =
2680 TextWeightSynthesis::for_style(segment.style, font.weight(), segment.font_size, 1.0);
2681 let natural = font
2682 .font
2683 .as_scaled(font.ab_glyph_px_size(segment.font_size))
2684 .h_scale_factor();
2685 let scale = run.horizontal_scale(natural, weight.apply_width(natural));
2686 let spacing = run.spacing(
2687 font.metadata
2688 .tracking
2689 .resolve(segment.style, segment.font_size),
2690 );
2691 for (index, cluster) in run.clusters.iter().enumerate() {
2692 visit(GlyphLayout {
2693 line_index: segment.line_index,
2694 start_offset: segment.range.start + cluster.range.start,
2695 end_offset: segment.range.start + cluster.range.end,
2696 x: segment.origin.x + cluster.x * scale + (index as f32 + 0.5) * spacing,
2697 y: segment.line_top,
2698 width: cluster.advance * scale,
2699 height: segment.line_height,
2700 });
2701 }
2702 return run.width(scale, spacing);
2703 }
2704 let shaped = segment.font.shaped_for(segment.style);
2705 let font = &*shaped;
2706 let weight =
2707 TextWeightSynthesis::for_style(segment.style, font.weight(), segment.font_size, 1.0);
2708 let glyph_size = font.ab_glyph_px_size(segment.font_size);
2709 let scaled = font.font.as_scaled(PxScale {
2710 x: weight.apply_width(glyph_size),
2711 y: glyph_size,
2712 });
2713 let spacing = font
2714 .metadata
2715 .tracking
2716 .resolve(segment.style, segment.font_size);
2717 let lead = run_lead_in(segment.text, spacing);
2718 let mut x = segment.origin.x + lead;
2719 let mut previous = None;
2720 for (offset, ch) in segment.text.char_indices() {
2721 let glyph = scaled.glyph_id(ch);
2722 if let Some(previous) = previous {
2723 x += scaled.kern(previous, glyph) + spacing;
2724 }
2725 let width = scaled.h_advance(glyph);
2726 visit(GlyphLayout {
2727 line_index: segment.line_index,
2728 start_offset: segment.range.start + offset,
2729 end_offset: segment.range.start + offset + ch.len_utf8(),
2730 x,
2731 y: segment.line_top,
2732 width,
2733 height: segment.line_height,
2734 });
2735 x += width;
2736 previous = Some(glyph);
2737 }
2738 (x - segment.origin.x + lead).max(0.0)
2739}
2740
2741fn segment_is_rtl(segment: &AnnotatedTextSegment<'_>) -> bool {
2742 requires_font_layout(segment.text, segment.style)
2743 && segment
2744 .style
2745 .paragraph_style
2746 .text_direction
2747 .resolve(segment.text)
2748 == cranpose_ui::text::ResolvedTextDirection::Rtl
2749}
2750
2751fn cursor_edges(glyph: &GlyphLayout, rtl: bool) -> [(f32, usize); 2] {
2752 let (start, end) = if rtl {
2753 (glyph.x + glyph.width, glyph.x)
2754 } else {
2755 (glyph.x, glyph.x + glyph.width)
2756 };
2757 [(start, glyph.start_offset), (end, glyph.end_offset)]
2758}
2759
2760pub(crate) fn layout_annotated_text_with_font_set<'a>(
2761 text: impl Into<StyledTextRef<'a>>,
2762 style: &TextStyle,
2763 fonts: impl Into<FontResolver<'a>>,
2764) -> TextLayoutResult {
2765 let text = text.into();
2766 let fonts = fonts.into();
2767 if text.span_styles.is_empty()
2768 && !requires_font_layout(text.text, style)
2769 && let Some(font) = fonts.single_font_for_text(text.text, style)
2770 {
2771 return layout_text_with_font(text.text, style, font);
2772 }
2773 let Some(layout) = AnnotatedTextLayout::new(
2774 text,
2775 style,
2776 resolve_font_size(style),
2777 1.0,
2778 measure_grid(),
2779 fonts,
2780 ) else {
2781 return fallback_layout_text(text.text, style);
2782 };
2783 let char_to_byte: Vec<usize> = text
2784 .text
2785 .char_indices()
2786 .map(|(offset, _)| offset)
2787 .chain(std::iter::once(text.text.len()))
2788 .collect();
2789 let mut glyph_x_positions = vec![0.0; char_to_byte.len()];
2790 let mut glyph_layouts = Vec::new();
2791 let mut lines = Vec::with_capacity(layout.lines.len());
2792 let mut width = 0.0_f32;
2793 for (index, line) in layout.lines.iter().enumerate() {
2794 let mut end_cursor = None;
2795 let advance = layout
2796 .walk_line(index, 0.0, |segment| {
2797 let rtl = segment_is_rtl(&segment);
2798 Some(walk_annotated_glyphs(&segment, |glyph| {
2799 let [(start, offset), (end, end_offset)] = cursor_edges(&glyph, rtl);
2800 let first = char_to_byte.partition_point(|byte| *byte < offset);
2801 let last = char_to_byte.partition_point(|byte| *byte < end_offset);
2802 glyph_x_positions[first..last].fill(start);
2803 if end_offset == line.range.end {
2804 end_cursor = Some(end);
2805 }
2806 glyph_layouts.push(glyph);
2807 }))
2808 })
2809 .unwrap_or(0.0);
2810 width = width.max(advance);
2811 if let Ok(index) = char_to_byte.binary_search(&line.range.end) {
2812 glyph_x_positions[index] = end_cursor.unwrap_or(advance);
2813 }
2814 lines.push(LineLayout {
2815 start_offset: line.range.start,
2816 end_offset: line.range.end,
2817 y: line.top,
2818 height: line.line_box.height,
2819 });
2820 }
2821 let metrics = layout.metrics(width);
2822 TextLayoutResult::new(
2823 text.text,
2824 TextLayoutData {
2825 width: metrics.width,
2826 height: metrics.height,
2827 line_height: metrics.line_height,
2828 glyph_x_positions,
2829 char_to_byte,
2830 lines,
2831 glyph_layouts,
2832 },
2833 )
2834}
2835
2836pub(crate) fn annotated_cursor_x_for_offset<'a>(
2837 text: impl Into<StyledTextRef<'a>>,
2838 style: &TextStyle,
2839 offset: usize,
2840 fonts: impl Into<FontResolver<'a>>,
2841) -> f32 {
2842 let text = text.into();
2843 let fonts = fonts.into();
2844 let offset = clamp_to_char_boundary(text.text, offset);
2845 if text.span_styles.is_empty()
2846 && !requires_font_layout(text.text, style)
2847 && let Some(font) = fonts.single_font_for_text(text.text, style)
2848 {
2849 return cursor_x_for_offset_with_font(text.text, style, offset, font);
2850 }
2851 let Some(layout) = AnnotatedTextLayout::new(
2852 text,
2853 style,
2854 resolve_font_size(style),
2855 1.0,
2856 measure_grid(),
2857 fonts,
2858 ) else {
2859 return fallback_cursor_x_for_offset(text.text, style, offset);
2860 };
2861 let line_index = layout
2862 .lines
2863 .iter()
2864 .position(|line| offset <= line.range.end)
2865 .unwrap_or_else(|| layout.lines.len().saturating_sub(1));
2866 let mut x = 0.0;
2867 let mut best = (0, false);
2868 let advance = layout
2869 .walk_line(line_index, 0.0, |segment| {
2870 let rtl = segment_is_rtl(&segment);
2871 Some(walk_annotated_glyphs(&segment, |glyph| {
2872 for (index, (position, boundary)) in
2873 cursor_edges(&glyph, rtl).into_iter().enumerate()
2874 {
2875 let rank = (boundary, index == 0);
2876 if boundary <= offset && rank >= best {
2877 best = rank;
2878 x = position;
2879 }
2880 }
2881 }))
2882 })
2883 .unwrap_or(0.0);
2884 if !requires_font_layout(text.text, style)
2885 && layout
2886 .lines
2887 .get(line_index)
2888 .is_some_and(|line| offset >= line.range.end)
2889 {
2890 advance
2891 } else {
2892 x
2893 }
2894}
2895
2896pub(crate) fn annotated_offset_for_position<'a>(
2897 text: impl Into<StyledTextRef<'a>>,
2898 style: &TextStyle,
2899 x: f32,
2900 y: f32,
2901 fonts: impl Into<FontResolver<'a>>,
2902) -> usize {
2903 let text = text.into();
2904 let fonts = fonts.into();
2905 if text.span_styles.is_empty()
2906 && !requires_font_layout(text.text, style)
2907 && let Some(font) = fonts.single_font_for_text(text.text, style)
2908 {
2909 return text_offset_for_position_with_font(text.text, style, x, y, font);
2910 }
2911 let Some(layout) = AnnotatedTextLayout::new(
2912 text,
2913 style,
2914 resolve_font_size(style),
2915 1.0,
2916 measure_grid(),
2917 fonts,
2918 ) else {
2919 return fallback_text_offset_for_position(text.text, style, x, y);
2920 };
2921 let line_index = layout
2922 .lines
2923 .iter()
2924 .position(|line| y < line.top + line.line_box.height)
2925 .unwrap_or_else(|| layout.lines.len().saturating_sub(1));
2926 let Some(line) = layout.lines.get(line_index) else {
2927 return 0;
2928 };
2929 let mut best = line.range.start;
2930 let mut distance = f32::INFINITY;
2931 let advance = layout
2932 .walk_line(line_index, 0.0, |segment| {
2933 let rtl = segment_is_rtl(&segment);
2934 Some(walk_annotated_glyphs(&segment, |glyph| {
2935 for (position, offset) in cursor_edges(&glyph, rtl) {
2936 let delta = (x - position).abs();
2937 if delta < distance {
2938 distance = delta;
2939 best = offset;
2940 }
2941 }
2942 }))
2943 })
2944 .unwrap_or(0.0);
2945 if !requires_font_layout(text.text, style) && (x - advance).abs() < distance {
2946 line.range.end
2947 } else {
2948 best
2949 }
2950}
2951
2952pub fn rasterize_text_to_image_with_font(
2955 text: &str,
2956 rect: Rect,
2957 style: &TextStyle,
2958 fallback_color: Color,
2959 font_size: f32,
2960 scale: f32,
2961 font: &impl Font,
2962) -> Option<ImageBitmap> {
2963 rasterize_text_to_image_impl(
2964 TextRasterImageRequest {
2965 text,
2966 rect,
2967 style,
2968 fallback_color,
2969 font_size,
2970 scale,
2971 },
2972 RasterFontRef {
2973 font,
2974 ab_glyph_scale_factor: 1.0,
2975 weight: FontWeight::NORMAL,
2976 style: FontStyle::Normal,
2977 tracking: &FontTracking::default(),
2978 },
2979 0,
2980 None,
2981 )
2982}
2983
2984struct GlyphPaint<'a> {
2985 brush: &'a Brush,
2986 alpha: f32,
2987 shadow: Option<Shadow>,
2988 brush_rect: Rect,
2989 canvas_origin: Point,
2990 scale: f32,
2991 static_text_motion: bool,
2992}
2993
2994struct GlyphRasterClip {
2995 width: f32,
2996 height: f32,
2997 shadow: Option<(Point, f32)>,
2998}
2999
3000impl GlyphRasterClip {
3001 fn new(width: u32, height: u32, paint: &GlyphPaint<'_>) -> Self {
3002 Self {
3003 width: width as f32,
3004 height: height as f32,
3005 shadow: paint.shadow.map(|shadow| {
3006 (
3007 Point::new(shadow.offset.x * paint.scale, shadow.offset.y * paint.scale),
3008 (shadow_blur_sigma((shadow.blur_radius * paint.scale).max(0.0)) * 3.0).ceil()
3009 + 1.0,
3010 )
3011 }),
3012 }
3013 }
3014
3015 fn intersects(&self, bounds: Rect) -> bool {
3016 let overlaps = |rect: Rect| {
3017 rect.x < self.width
3018 && rect.y < self.height
3019 && rect.x + rect.width > 0.0
3020 && rect.y + rect.height > 0.0
3021 };
3022 overlaps(bounds)
3023 || self.shadow.is_some_and(|(offset, margin)| {
3024 overlaps(Rect {
3025 x: bounds.x + offset.x - margin,
3026 y: bounds.y + offset.y - margin,
3027 width: bounds.width + 2.0 * margin,
3028 height: bounds.height + 2.0 * margin,
3029 })
3030 })
3031 }
3032}
3033
3034enum TextRasterPixels {
3035 Solid(Vec<u8>),
3036 Blended(Vec<[f32; 4]>),
3037}
3038
3039struct TextRasterCanvas {
3040 width: u32,
3041 height: u32,
3042 pixels: TextRasterPixels,
3043}
3044
3045impl TextRasterCanvas {
3046 fn new(width: u32, height: u32, solid: bool) -> Self {
3047 let len = width as usize * height as usize;
3048 Self {
3049 width,
3050 height,
3051 pixels: if solid {
3052 TextRasterPixels::Solid(vec![0; len * 4])
3053 } else {
3054 TextRasterPixels::Blended(vec![[0.0; 4]; len])
3055 },
3056 }
3057 }
3058
3059 fn paint(&mut self, mask: &GlyphMask, paint: &GlyphPaint<'_>) {
3060 match &mut self.pixels {
3061 TextRasterPixels::Solid(pixels) => {
3062 if let Brush::Solid(color) = paint.brush {
3063 draw_mask_glyph_solid_u8(
3064 pixels,
3065 self.width,
3066 self.height,
3067 mask,
3068 color_to_rgba(*color),
3069 paint.alpha,
3070 );
3071 }
3072 }
3073 TextRasterPixels::Blended(pixels) => {
3074 if let Some(shadow) = paint.shadow {
3075 draw_shadow_mask(
3076 pixels,
3077 self.width,
3078 self.height,
3079 mask,
3080 shadow,
3081 paint.scale,
3082 paint.static_text_motion,
3083 );
3084 }
3085 draw_mask_glyph(pixels, self.width, self.height, mask, paint);
3086 }
3087 }
3088 }
3089
3090 fn into_image(self) -> Option<ImageBitmap> {
3091 let pixels = match self.pixels {
3092 TextRasterPixels::Solid(pixels) => pixels,
3093 TextRasterPixels::Blended(canvas) => {
3094 let mut pixels = Vec::with_capacity(canvas.len() * 4);
3095 for pixel in canvas {
3096 pixels.extend(
3097 pixel.map(|channel| (channel.clamp(0.0, 1.0) * 255.0).round() as u8),
3098 );
3099 }
3100 pixels
3101 }
3102 };
3103 ImageBitmap::from_rgba8(self.width, self.height, pixels).ok()
3104 }
3105}
3106
3107struct TextRasterImageRequest<'a> {
3108 text: &'a str,
3109 rect: Rect,
3110 style: &'a TextStyle,
3111 fallback_color: Color,
3112 font_size: f32,
3113 scale: f32,
3114}
3115
3116fn rasterize_text_to_image_impl(
3117 request: TextRasterImageRequest<'_>,
3118 font_ref: RasterFontRef<'_, impl Font>,
3119 font_cache_key: u64,
3120 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
3121) -> Option<ImageBitmap> {
3122 let TextRasterImageRequest {
3123 text,
3124 rect,
3125 style,
3126 fallback_color,
3127 font_size,
3128 scale,
3129 } = request;
3130
3131 if text_render_request_is_degenerate(text.is_empty(), rect, font_size, scale) {
3132 return None;
3133 }
3134
3135 let width = rect.width.ceil().max(1.0) as u32;
3136 let height = rect.height.ceil().max(1.0) as u32;
3137
3138 let fallback_brush = Brush::solid(fallback_color);
3139 let (brush, brush_alpha_multiplier) = match style.span_style.brush.as_ref() {
3140 Some(brush) => (brush, style.span_style.alpha.unwrap_or(1.0).clamp(0.0, 1.0)),
3141 None => (&fallback_brush, 1.0),
3142 };
3143 let raster_style = match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
3144 TextDrawStyle::Fill => GlyphRasterStyle::Fill,
3145 TextDrawStyle::Stroke { width } => {
3146 if width.is_finite() && width > 0.0 {
3147 GlyphRasterStyle::Stroke {
3148 width_px: width * scale,
3149 }
3150 } else {
3151 GlyphRasterStyle::Fill
3152 }
3153 }
3154 };
3155 let shadow = style
3156 .span_style
3157 .shadow
3158 .filter(|shadow| shadow.color.3 > 0.0);
3159 let static_text_motion = style
3160 .paragraph_style
3161 .text_motion
3162 .unwrap_or(TextMotion::Static)
3163 == TextMotion::Static;
3164
3165 let origin_x = if static_text_motion {
3166 0.0
3167 } else {
3168 rect.x.fract()
3169 };
3170 let origin_y = if static_text_motion {
3171 0.0
3172 } else {
3173 rect.y.fract()
3174 };
3175
3176 let font = font_ref.font;
3177 let font_px_size = font_size * scale * font_ref.ab_glyph_scale_factor;
3178 let letter_spacing = font_ref.tracking.resolve(style, font_size) * scale;
3179 let align_fraction = cranpose_ui::text::text_align_fraction(style, text);
3180 let weight_synthesis = TextWeightSynthesis::for_style(style, font_ref.weight, font_size, scale);
3181 let style_synthesis = TextStyleSynthesis::for_style(style, font_ref.style, font_size, scale);
3182 let metrics = vertical_metrics(font, font_px_size);
3183 let line_box = line_box_for(style, metrics, asked_line_height(style, scale), 1.0);
3184 let line_height = line_box.height;
3185 let first_baseline_y = line_box.first_baseline();
3186
3187 let mut canvas = TextRasterCanvas::new(
3188 width,
3189 height,
3190 matches!(brush, Brush::Solid(_)) && shadow.is_none(),
3191 );
3192 let paint = GlyphPaint {
3193 brush,
3194 alpha: brush_alpha_multiplier,
3195 shadow,
3196 brush_rect: rect,
3197 canvas_origin: Point::new(rect.x, rect.y),
3198 scale,
3199 static_text_motion,
3200 };
3201 visit_text_glyph_masks(
3202 text,
3203 font,
3204 font_cache_key,
3205 font_px_size,
3206 line_height,
3207 first_baseline_y,
3208 origin_x,
3209 origin_y,
3210 letter_spacing,
3211 align_fraction,
3212 static_text_motion,
3213 raster_style,
3214 weight_synthesis,
3215 style_synthesis,
3216 glyph_cache,
3217 None,
3218 |mask| canvas.paint(mask, &paint),
3219 );
3220 canvas.into_image()
3221}
3222
3223#[expect(clippy::too_many_arguments)]
3224fn visit_text_segment_masks(
3225 text: &str,
3226 local_rect: Rect,
3227 style: &TextStyle,
3228 font_size: f32,
3229 scale: f32,
3230 font: &SoftwareTextFont,
3231 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
3232 clip: &GlyphRasterClip,
3233 visit: impl FnMut(&GlyphMask),
3234) -> f32 {
3235 if text_render_request_is_degenerate(text.is_empty(), local_rect, font_size, scale) {
3236 return 0.0;
3237 }
3238 let raster_style = match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
3239 TextDrawStyle::Stroke { width } if width.is_finite() && width > 0.0 => {
3240 GlyphRasterStyle::Stroke {
3241 width_px: width * scale,
3242 }
3243 }
3244 _ => GlyphRasterStyle::Fill,
3245 };
3246 let static_text_motion = style
3247 .paragraph_style
3248 .text_motion
3249 .unwrap_or(TextMotion::Static)
3250 == TextMotion::Static;
3251 let shaped = font.shaped_for(style);
3252 let font = &*shaped;
3253 let m = text_segment_metrics(text, local_rect, style, font_size, scale, font);
3254 #[cfg(feature = "text-shaping")]
3255 if requires_font_layout(text, style) {
3256 let mut glyph_cache = glyph_cache;
3257 let mut visit = visit;
3258 return visit_shaped_glyphs(text, style, font, &m, local_rect.x, |glyph| {
3259 let glyph = align_glyph_for_text_motion(glyph, static_text_motion);
3260 if let Some(mask) = raster_glyph_mask(
3261 if static_text_motion {
3262 glyph_cache.as_deref_mut()
3263 } else {
3264 None
3265 },
3266 font.content_hash(),
3267 &font.font,
3268 &glyph,
3269 raster_style,
3270 m.weight_synthesis,
3271 m.style_synthesis,
3272 Some(clip),
3273 ) {
3274 visit(&mask);
3275 }
3276 ControlFlow::Continue(())
3277 })
3278 .unwrap_or(0.0);
3279 }
3280 visit_text_glyph_masks(
3281 text,
3282 &font.font,
3283 font.content_hash(),
3284 m.font_px_size,
3285 m.line_height,
3286 m.first_baseline_y,
3287 local_rect.x,
3288 0.0,
3289 m.letter_spacing,
3290 m.align_fraction,
3291 static_text_motion,
3292 raster_style,
3293 m.weight_synthesis,
3294 m.style_synthesis,
3295 glyph_cache,
3296 Some(clip),
3297 visit,
3298 )
3299}
3300
3301#[expect(clippy::too_many_arguments)]
3302fn collect_text_segment_solid_atlas_glyphs(
3303 text: &str,
3304 local_rect: Rect,
3305 style: &TextStyle,
3306 color: Color,
3307 font_size: f32,
3308 scale: f32,
3309 font: &SoftwareTextFont,
3310 glyph_cache: &mut SoftwareGlyphRasterCache,
3311 out: &mut Vec<SoftwareGlyphAtlasGlyph>,
3312) -> Option<f32> {
3313 let request = AtlasSegmentRequest {
3314 text,
3315 local_rect,
3316 style,
3317 font_size,
3318 scale,
3319 font,
3320 };
3321 collect_atlas_segment(request, glyph_cache, out, |cache, glyph, out| {
3322 let mask = match cache.get(&glyph.key, &glyph.glyph) {
3323 Some(mask) => mask,
3324 None => {
3325 let Some(mask) = glyph.build_mask() else {
3326 return ControlFlow::Continue(());
3327 };
3328 cache.put(glyph.key, &glyph.glyph, mask)
3329 }
3330 };
3331 if let Some(key) = glyph_atlas_key_from_mask_key(glyph.key)
3332 && mask.width != 0
3333 && mask.height != 0
3334 {
3335 out.push(SoftwareGlyphAtlasGlyph {
3336 key,
3337 mask: SoftwareGlyphAtlasMask {
3338 alpha: mask.alpha,
3339 width: mask.width,
3340 height: mask.height,
3341 },
3342 x: mask.origin_x,
3343 y: mask.origin_y,
3344 color,
3345 });
3346 }
3347 ControlFlow::Continue(())
3348 })
3349}
3350
3351#[expect(clippy::too_many_arguments)]
3352fn collect_text_segment_cached_solid_atlas_placements(
3353 text: &str,
3354 local_rect: Rect,
3355 style: &TextStyle,
3356 color: Color,
3357 font_size: f32,
3358 scale: f32,
3359 font: &SoftwareTextFont,
3360 glyph_cache: &mut SoftwareGlyphRasterCache,
3361 out: &mut Vec<SoftwareGlyphAtlasPlacement>,
3362) -> Option<f32> {
3363 let request = AtlasSegmentRequest {
3364 text,
3365 local_rect,
3366 style,
3367 font_size,
3368 scale,
3369 font,
3370 };
3371 collect_atlas_segment(request, glyph_cache, out, |cache, glyph, out| {
3372 let Some((key, x, y, width, height)) = cache.get_atlas_placement(&glyph.key, &glyph.glyph)
3373 else {
3374 return if glyph.has_outline() {
3377 ControlFlow::Break(())
3378 } else {
3379 ControlFlow::Continue(())
3380 };
3381 };
3382 if width != 0 && height != 0 {
3383 out.push(SoftwareGlyphAtlasPlacement {
3384 key,
3385 x,
3386 y,
3387 width,
3388 height,
3389 color,
3390 });
3391 }
3392 ControlFlow::Continue(())
3393 })
3394}
3395
3396#[expect(clippy::too_many_arguments)]
3397fn collect_text_segment_solid_atlas_run(
3398 text: &str,
3399 local_rect: Rect,
3400 style: &TextStyle,
3401 color: Color,
3402 font_size: f32,
3403 scale: f32,
3404 font: &SoftwareTextFont,
3405 glyph_cache: &mut SoftwareGlyphRasterCache,
3406 out: &mut Vec<SoftwareGlyphAtlasRunGlyph>,
3407) -> Option<f32> {
3408 let request = AtlasSegmentRequest {
3409 text,
3410 local_rect,
3411 style,
3412 font_size,
3413 scale,
3414 font,
3415 };
3416 glyph_cache.segment_metrics.clear();
3417 let transparent = color.3 <= 0.0;
3418 collect_atlas_segment(request, glyph_cache, out, |cache, glyph, out| {
3419 if transparent {
3420 return ControlFlow::Continue(());
3421 }
3422 let metrics = match cache.segment_metrics.get(glyph.ch) {
3423 Some(metrics) => metrics,
3424 None => match cache.get_atlas_metrics(&glyph.key) {
3425 Some(metrics) => {
3426 cache.segment_metrics.insert(glyph.ch, metrics);
3427 metrics
3428 }
3429 None => {
3430 if let Some(new) = rasterize_atlas_run_glyph(cache, glyph)
3431 && new.mask.width != 0
3432 && new.mask.height != 0
3433 {
3434 out.push(SoftwareGlyphAtlasRunGlyph::New(SoftwareGlyphAtlasGlyph {
3435 color,
3436 ..new
3437 }));
3438 }
3439 return ControlFlow::Continue(());
3440 }
3441 },
3442 };
3443 if metrics.width != 0 && metrics.height != 0 {
3444 out.push(SoftwareGlyphAtlasRunGlyph::Cached(
3445 metrics.placement(&glyph.glyph, color),
3446 ));
3447 }
3448 ControlFlow::Continue(())
3449 })
3450}
3451
3452fn rasterize_atlas_run_glyph(
3455 cache: &mut SoftwareGlyphRasterCache,
3456 glyph: &AtlasSegmentGlyph<'_>,
3457) -> Option<SoftwareGlyphAtlasGlyph> {
3458 if !glyph.has_outline() {
3459 return None;
3460 }
3461 let mask = glyph.build_mask()?;
3462 let mask = cache.put(glyph.key, &glyph.glyph, mask);
3463 let key = glyph_atlas_key_from_mask_key(glyph.key)?;
3464 let (glyph_x, glyph_y) = static_glyph_pixel_origin(&glyph.glyph);
3465 cache.segment_metrics.insert(
3466 glyph.ch,
3467 CachedAtlasGlyphMetrics {
3468 key,
3469 width: mask.width,
3470 height: mask.height,
3471 origin_offset_x: mask.origin_x - glyph_x,
3472 origin_offset_y: mask.origin_y - glyph_y,
3473 },
3474 );
3475 Some(SoftwareGlyphAtlasGlyph {
3476 key,
3477 mask: SoftwareGlyphAtlasMask {
3478 alpha: mask.alpha,
3479 width: mask.width,
3480 height: mask.height,
3481 },
3482 x: mask.origin_x,
3483 y: mask.origin_y,
3484 color: Color::WHITE,
3485 })
3486}
3487
3488#[derive(Clone, Copy)]
3490struct AtlasSegmentRequest<'a> {
3491 text: &'a str,
3492 local_rect: Rect,
3493 style: &'a TextStyle,
3494 font_size: f32,
3495 scale: f32,
3496 font: &'a SoftwareTextFont,
3497}
3498
3499struct AtlasSegmentGlyph<'a> {
3502 ch: char,
3503 glyph: Glyph,
3504 key: GlyphMaskCacheKey,
3505 font: &'a KernedFont,
3506 metrics: &'a TextSegmentMetrics,
3507}
3508
3509impl AtlasSegmentGlyph<'_> {
3510 fn has_outline(&self) -> bool {
3511 self.font.outline(self.glyph.id).is_some()
3512 }
3513
3514 fn build_mask(&self) -> Option<GlyphMask> {
3515 build_complete_glyph_mask(
3516 self.font,
3517 &self.glyph,
3518 GlyphRasterStyle::Fill,
3519 self.metrics.weight_synthesis,
3520 self.metrics.style_synthesis,
3521 None,
3522 )
3523 }
3524}
3525
3526impl<'a> AtlasSegmentGlyph<'a> {
3527 fn new(
3528 ch: char,
3529 glyph: Glyph,
3530 font: &'a SoftwareTextFont,
3531 metrics: &'a TextSegmentMetrics,
3532 ) -> Self {
3533 let glyph = align_glyph_for_text_motion(glyph, true);
3534 Self {
3535 ch,
3536 key: glyph_mask_cache_key(
3537 font.content_hash(),
3538 &glyph,
3539 GlyphRasterStyle::Fill,
3540 metrics.weight_synthesis,
3541 metrics.style_synthesis,
3542 ),
3543 glyph,
3544 font: &font.font,
3545 metrics,
3546 }
3547 }
3548}
3549
3550fn collect_atlas_segment<T>(
3556 request: AtlasSegmentRequest<'_>,
3557 glyph_cache: &mut SoftwareGlyphRasterCache,
3558 out: &mut Vec<T>,
3559 mut visit: impl FnMut(
3560 &mut SoftwareGlyphRasterCache,
3561 &AtlasSegmentGlyph<'_>,
3562 &mut Vec<T>,
3563 ) -> ControlFlow<()>,
3564) -> Option<f32> {
3565 let AtlasSegmentRequest {
3566 text,
3567 local_rect,
3568 style,
3569 font_size,
3570 scale,
3571 font,
3572 } = request;
3573 if text_render_request_is_degenerate(text.is_empty(), local_rect, font_size, scale) {
3574 return Some(0.0);
3575 }
3576 if !text_segment_supports_solid_atlas(style) {
3577 return None;
3578 }
3579
3580 let shaped = font.shaped_for(style);
3581 let font = &*shaped;
3582 let m = text_segment_metrics(text, local_rect, style, font_size, scale, font);
3583 let font_hash = font.content_hash();
3584 let kerned = &font.font;
3585 let origin_x = local_rect.x.round();
3586 let initial_len = out.len();
3587 #[cfg(feature = "text-shaping")]
3588 if requires_font_layout(text, style) {
3589 let result = visit_shaped_glyphs(text, style, font, &m, origin_x, |glyph| {
3590 let glyph = AtlasSegmentGlyph::new('\0', glyph, font, &m);
3591 visit(glyph_cache, &glyph, out)
3592 });
3593 if result.is_none() {
3594 out.truncate(initial_len);
3595 }
3596 return result;
3597 }
3598 let px_scale = PxScale::from(m.font_px_size);
3599 let scaled_font = kerned.as_scaled(px_scale);
3600 let h_scale = scaled_font.h_scale_factor();
3601 let line_offsets =
3602 line_alignment_offsets(&scaled_font, text, m.letter_spacing, m.align_fraction);
3603 let mut max_advance = 0.0f32;
3604 for (line_idx, line) in text.split('\n').enumerate() {
3605 let baseline_y = m.first_baseline_y + line_idx as f32 * m.line_height;
3606 let lead_in = run_lead_in(line, m.letter_spacing);
3607 let mut caret_x = origin_x + line_offset(&line_offsets, line_idx) + lead_in;
3608 let mut previous: Option<(char, GlyphId)> = None;
3609 for ch in line.chars() {
3610 let (glyph_id, advance) = kerned.glyph_advance(ch);
3611 if let Some(previous) = previous {
3612 let kern = kerned
3613 .ascii_kern(previous, (ch, glyph_id))
3614 .unwrap_or_else(|| {
3615 glyph_cache.kern_unscaled(font_hash, kerned, previous.1, glyph_id)
3616 });
3617 caret_x += kern * h_scale + m.letter_spacing;
3618 }
3619 let glyph = glyph_id.with_scale_and_position(px_scale, point(caret_x, baseline_y));
3620 caret_x += advance * h_scale;
3621 previous = Some((ch, glyph_id));
3622 let segment_glyph = AtlasSegmentGlyph::new(ch, glyph, font, &m);
3623 if visit(glyph_cache, &segment_glyph, out).is_break() {
3624 out.truncate(initial_len);
3625 return None;
3626 }
3627 }
3628 max_advance = max_advance.max((caret_x - origin_x + lead_in).max(0.0));
3629 }
3630 Some(max_advance)
3631}
3632
3633fn resolve_font_size(style: &TextStyle) -> f32 {
3634 style.resolve_font_size(14.0)
3635}
3636
3637#[cfg(feature = "text-shaping")]
3638fn shaped_run_width(
3639 run: &crate::text_shaping::ShapedRun,
3640 font: &SoftwareTextFont,
3641 style: &TextStyle,
3642 font_size: f32,
3643 scale: f32,
3644) -> f32 {
3645 let scaled = font
3646 .font
3647 .as_scaled(font.ab_glyph_px_size(font_size) * scale);
3648 let weight = TextWeightSynthesis::for_style(style, font.weight(), font_size, scale);
3649 run.width(
3650 run.horizontal_scale(
3651 scaled.h_scale_factor(),
3652 weight.apply_width(scaled.h_scale_factor()),
3653 ),
3654 font.metadata.tracking.resolve(style, font_size) * scale,
3655 )
3656}
3657
3658#[cfg(feature = "text-shaping")]
3659fn measure_shaped_text(
3660 text: &str,
3661 style: &TextStyle,
3662 font_size: f32,
3663 font: &SoftwareTextFont,
3664) -> TextMetrics {
3665 let line_box = font_line_box(style, font, font_size);
3666 let mut width = 0.0_f32;
3667 let mut count = 0;
3668 for line in text.split('\n') {
3669 count += 1;
3670 if let Some(run) = font.shaped_run(line, style) {
3671 let overhang = if line.is_empty() {
3672 0.0
3673 } else {
3674 TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0)
3675 .visual_overhang_px()
3676 };
3677 width = width.max(shaped_run_width(&run, font, style, font_size, 1.0) + overhang);
3678 }
3679 }
3680 TextMetrics {
3681 width,
3682 height: line_box.block_height(count),
3683 line_height: line_box.height,
3684 line_count: count,
3685 }
3686}
3687
3688#[cfg(feature = "text-shaping")]
3689fn visit_shaped_glyphs(
3690 text: &str,
3691 style: &TextStyle,
3692 font: &SoftwareTextFont,
3693 m: &TextSegmentMetrics,
3694 origin_x: f32,
3695 mut visit: impl FnMut(Glyph) -> ControlFlow<()>,
3696) -> Option<f32> {
3697 let scaled = font.font.as_scaled(m.font_px_size);
3698 let natural_scale = scaled.h_scale_factor();
3699 let synthesized_scale = m.weight_synthesis.apply_width(natural_scale);
3700 let scale_y = scaled.v_scale_factor();
3701 let mut width = 0.0_f32;
3702 for line in text.split('\n') {
3703 let run = font.shaped_run(line, style)?;
3704 width = width.max(run.width(
3705 run.horizontal_scale(natural_scale, synthesized_scale),
3706 m.letter_spacing,
3707 ));
3708 }
3709 for (index, line) in text.split('\n').enumerate() {
3710 let run = font.shaped_run(line, style)?;
3711 let scale_x = run.horizontal_scale(natural_scale, synthesized_scale);
3712 let x = origin_x + (width - run.width(scale_x, m.letter_spacing)) * m.align_fraction;
3713 let y = m.first_baseline_y + index as f32 * m.line_height;
3714 let mut result = ControlFlow::Continue(());
3715 run.visit(scale_x, scale_y, m.letter_spacing, |id, dx, dy| {
3716 if result.is_continue() {
3717 result = visit(id.with_scale_and_position(m.font_px_size, point(x + dx, y + dy)));
3718 }
3719 });
3720 if result.is_break() {
3721 return None;
3722 }
3723 }
3724 Some(width)
3725}
3726
3727fn line_box_for(
3728 style: &TextStyle,
3729 metrics: crate::font_layout::FontVerticalMetrics,
3730 line_height: f32,
3731 grid: f32,
3732) -> cranpose_ui::text::LineBox {
3733 cranpose_ui::text::line_box(
3734 style,
3735 cranpose_ui::text::FontExtent::new(metrics.ascent, -metrics.descent, metrics.line_gap),
3736 line_height,
3737 grid,
3738 )
3739}
3740
3741pub(crate) fn font_extent(
3742 font: &SoftwareTextFont,
3743 font_size: f32,
3744) -> cranpose_ui::text::FontExtent {
3745 let metrics = vertical_metrics(&font.font, font.ab_glyph_px_size(font_size));
3746 cranpose_ui::text::FontExtent::new(metrics.ascent, -metrics.descent, metrics.line_gap)
3747}
3748
3749pub(crate) fn font_line_box(
3751 style: &TextStyle,
3752 font: &SoftwareTextFont,
3753 font_size: f32,
3754) -> cranpose_ui::text::LineBox {
3755 line_box_for(
3756 style,
3757 crate::font_layout::vertical_metrics(&font.font, font.ab_glyph_px_size(font_size)),
3758 asked_line_height(style, 1.0),
3759 measure_grid(),
3760 )
3761}
3762
3763pub(crate) fn font_glyph_line_box(style: &TextStyle, font: &SoftwareTextFont) -> (f32, f32) {
3768 let font_size = resolve_font_size(style);
3769 let metrics =
3770 crate::font_layout::vertical_metrics(&font.font, font.ab_glyph_px_size(font_size));
3771 let resolved = font_line_box(style, font, font_size);
3772 let height = metrics.natural_line_height.min(resolved.height).max(1.0);
3773 (resolved.first_baseline() - metrics.ascent, height)
3774}
3775
3776fn measure_grid() -> f32 {
3777 if cranpose_ui::has_current_app_context() {
3778 cranpose_ui::current_density()
3779 } else {
3780 1.0
3781 }
3782}
3783
3784fn resolve_line_height(style: &TextStyle, font_size: f32) -> f32 {
3785 style.resolve_line_height(14.0, font_size)
3786}
3787
3788pub(crate) fn asked_line_height(style: &TextStyle, scale: f32) -> f32 {
3791 (style.resolve_line_height(14.0, f32::NAN) * scale).max(1.0)
3792}
3793
3794fn resolve_letter_spacing(style: &TextStyle, font_size: f32) -> f32 {
3795 let _ = font_size;
3796 style.resolve_letter_spacing(14.0)
3797}
3798
3799fn run_tracking(char_count: usize, letter_spacing: f32) -> f32 {
3800 char_count as f32 * letter_spacing
3801}
3802
3803fn run_lead_in(line: &str, letter_spacing: f32) -> f32 {
3804 if line.is_empty() {
3805 0.0
3806 } else {
3807 letter_spacing * 0.5
3808 }
3809}
3810
3811fn fallback_char_width(font_size: f32) -> f32 {
3812 font_size.max(1.0) * 0.55
3813}
3814
3815fn fallback_line_height(style: &TextStyle, font_size: f32) -> f32 {
3816 resolve_line_height(style, font_size.max(1.0) * 1.2)
3817}
3818
3819fn fallback_text_metrics(text: &str, style: &TextStyle, font_size: f32) -> TextMetrics {
3820 let line_height = fallback_line_height(style, font_size);
3821 let char_width = fallback_char_width(font_size);
3822 let letter_spacing = resolve_letter_spacing(style, font_size);
3823 let mut line_count = 0usize;
3824 let mut max_width = 0.0f32;
3825
3826 for line in text.split('\n') {
3827 line_count += 1;
3828 let char_count = line.chars().count();
3829 let spacing = run_tracking(char_count, letter_spacing);
3830 max_width = max_width.max(char_count as f32 * char_width + spacing);
3831 }
3832
3833 let line_count = line_count.max(1);
3834 TextMetrics {
3835 width: max_width,
3836 height: line_count as f32 * line_height,
3837 line_height,
3838 line_count,
3839 }
3840}
3841
3842fn fallback_cursor_x_for_offset(text: &str, style: &TextStyle, offset: usize) -> f32 {
3843 let font_size = resolve_font_size(style);
3844 let clamped = clamp_to_char_boundary(text, offset.min(text.len()));
3845 let line_start = text[..clamped].rfind('\n').map_or(0, |index| index + 1);
3846 let char_count = text[line_start..clamped].chars().count();
3847 let spacing = run_tracking(char_count, resolve_letter_spacing(style, font_size));
3848 char_count as f32 * fallback_char_width(font_size) + spacing
3849}
3850
3851fn fallback_text_offset_for_position(text: &str, style: &TextStyle, x: f32, y: f32) -> usize {
3852 if text.is_empty() {
3853 return 0;
3854 }
3855
3856 let font_size = resolve_font_size(style);
3857 let line_height = fallback_line_height(style, font_size);
3858 let line_index = (y / line_height).floor().max(0.0) as usize;
3859 let lines: Vec<&str> = text.split('\n').collect();
3860 let target_line = line_index.min(lines.len().saturating_sub(1));
3861
3862 let mut line_start_byte = 0;
3863 for line in lines.iter().take(target_line) {
3864 line_start_byte += line.len() + 1;
3865 }
3866
3867 let line_text = lines.get(target_line).copied().unwrap_or("");
3868 if line_text.is_empty() {
3869 return line_start_byte;
3870 }
3871
3872 let advance =
3873 (fallback_char_width(font_size) + resolve_letter_spacing(style, font_size)).max(1.0);
3874 let target_char = (x / advance).round().max(0.0) as usize;
3875 line_start_byte + byte_offset_for_char_index(line_text, target_char)
3876}
3877
3878fn fallback_layout_text(text: &str, style: &TextStyle) -> TextLayoutResult {
3879 let font_size = resolve_font_size(style);
3880 let line_height = fallback_line_height(style, font_size);
3881 let char_width = fallback_char_width(font_size);
3882 let letter_spacing = resolve_letter_spacing(style, font_size);
3883
3884 let mut glyph_x_positions = Vec::new();
3885 let mut char_to_byte = Vec::new();
3886 let mut glyph_layouts = Vec::new();
3887 let mut lines = Vec::new();
3888 let mut current_x = 0.0f32;
3889 let mut line_start = 0;
3890 let mut y = 0.0f32;
3891
3892 let mut iter = text.char_indices().peekable();
3893 while let Some((byte_offset, ch)) = iter.next() {
3894 glyph_x_positions.push(current_x);
3895 char_to_byte.push(byte_offset);
3896
3897 if ch == '\n' {
3898 lines.push(LineLayout {
3899 start_offset: line_start,
3900 end_offset: byte_offset,
3901 y,
3902 height: line_height,
3903 });
3904 line_start = byte_offset + 1;
3905 y += line_height;
3906 current_x = 0.0;
3907 } else {
3908 glyph_layouts.push(GlyphLayout {
3909 line_index: lines.len(),
3910 start_offset: byte_offset,
3911 end_offset: byte_offset + ch.len_utf8(),
3912 x: current_x,
3913 y,
3914 width: char_width,
3915 height: line_height,
3916 });
3917 current_x += char_width;
3918 if let Some((_, next)) = iter.peek()
3919 && *next != '\n'
3920 {
3921 current_x += letter_spacing;
3922 }
3923 }
3924 }
3925
3926 glyph_x_positions.push(current_x);
3927 char_to_byte.push(text.len());
3928 lines.push(LineLayout {
3929 start_offset: line_start,
3930 end_offset: text.len(),
3931 y,
3932 height: line_height,
3933 });
3934
3935 let metrics = fallback_text_metrics(text, style, font_size);
3936 TextLayoutResult::new(
3937 text,
3938 TextLayoutData {
3939 width: metrics.width,
3940 height: metrics.height,
3941 line_height,
3942 glyph_x_positions,
3943 char_to_byte,
3944 glyph_layouts,
3945 lines,
3946 },
3947 )
3948}
3949
3950fn style_allows_prefix_widths(style: &TextStyle) -> bool {
3951 !matches!(
3952 style
3953 .paragraph_style
3954 .platform_style
3955 .and_then(|platform| platform.shaping),
3956 Some(TextShaping::Advanced)
3957 )
3958}
3959
3960fn cached_line_advance_width(
3961 font: &SoftwareTextFont,
3962 text: &str,
3963 glyph_font_size: f32,
3964 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
3965) -> f32 {
3966 let scaled_font = font.font.as_scaled(PxScale::from(glyph_font_size));
3967 let h_scale = scaled_font.h_scale_factor();
3968 let mut width = 0.0f32;
3969 let mut previous = None;
3970
3971 for ch in text.chars() {
3972 let metrics = glyph_metrics.glyph_metrics(font, &scaled_font, ch);
3973 if let Some(previous) = previous {
3974 width +=
3975 glyph_metrics.kern(font, &scaled_font, previous, (ch, metrics.glyph_id)) * h_scale;
3976 }
3977 width += metrics.advance_unscaled * h_scale;
3978 previous = Some((ch, metrics.glyph_id));
3979 }
3980
3981 width.max(0.0)
3982}
3983
3984trait PrefixWidthSink {
3988 fn push(&mut self, separator: f32, prefix_width: f32);
3989}
3990
3991struct PrefixWidthVecs {
3994 prefix_widths: Vec<f32>,
3995 separator_before: Vec<f32>,
3996}
3997
3998impl PrefixWidthSink for PrefixWidthVecs {
3999 fn push(&mut self, separator: f32, prefix_width: f32) {
4000 self.separator_before.push(separator);
4001 self.prefix_widths.push(prefix_width);
4002 }
4003}
4004
4005#[derive(Default)]
4008struct LineWidthSink {
4009 first_separator: Option<f32>,
4010 last_width: f32,
4011 non_finite: bool,
4012}
4013
4014impl PrefixWidthSink for LineWidthSink {
4015 fn push(&mut self, separator: f32, prefix_width: f32) {
4016 self.first_separator.get_or_insert(separator);
4017 self.last_width = prefix_width;
4018 self.non_finite |= !separator.is_finite() || !prefix_width.is_finite();
4019 }
4020}
4021
4022impl LineWidthSink {
4023 fn width(&self, non_empty_overhang: f32) -> Option<f32> {
4026 let overhang = non_empty_overhang.max(0.0);
4027 if self.non_finite || !overhang.is_finite() {
4028 return None;
4029 }
4030 Some(self.first_separator.map_or(0.0, |separator| {
4031 (self.last_width - separator).max(0.0) + overhang
4032 }))
4033 }
4034}
4035
4036fn walk_line_prefix_widths(
4039 text: &AnnotatedString,
4040 line_range: std::ops::Range<usize>,
4041 style: &TextStyle,
4042 fonts: &SoftwareTextFontSet,
4043 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
4044 sink: &mut impl PrefixWidthSink,
4045) -> f32 {
4046 let mut boundaries = text.span_boundaries();
4047 boundaries.push(line_range.start);
4048 boundaries.push(line_range.end);
4049 boundaries.sort_unstable();
4050 boundaries.dedup();
4051 boundaries.retain(|offset| {
4052 *offset >= line_range.start
4053 && *offset <= line_range.end
4054 && text.text.is_char_boundary(*offset)
4055 });
4056
4057 let mut walk = PrefixWidthWalk {
4058 sink,
4059 width: 0.0,
4060 non_empty_overhang: 0.0,
4061 };
4062 for range in boundaries.windows(2) {
4063 let start = range[0];
4064 let end = range[1];
4065 if start >= end {
4066 continue;
4067 }
4068 let segment = &text.text[start..end];
4069 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
4070 append_prefix_width_segment(segment, &segment_style, fonts, glyph_metrics, &mut walk);
4071 }
4072 walk.non_empty_overhang
4073}
4074
4075struct PrefixWidthWalk<'a, S> {
4076 sink: &'a mut S,
4077 width: f32,
4078 non_empty_overhang: f32,
4079}
4080
4081impl<S: PrefixWidthSink> PrefixWidthWalk<'_, S> {
4082 fn advance(&mut self, separator: f32, step: f32) {
4084 self.width += step;
4085 self.sink.push(separator, self.width.max(0.0));
4086 }
4087}
4088
4089fn append_prefix_width_segment<S: PrefixWidthSink>(
4090 segment: &str,
4091 style: &TextStyle,
4092 fonts: &SoftwareTextFontSet,
4093 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
4094 walk: &mut PrefixWidthWalk<'_, S>,
4095) {
4096 if segment.is_empty() {
4097 return;
4098 }
4099
4100 let font_size = resolve_font_size(style);
4101 if fonts
4102 .visit_font_runs(segment, style, |range, font| {
4103 append_font_prefix_width_segment(
4104 &segment[range],
4105 style,
4106 font_size,
4107 font,
4108 glyph_metrics,
4109 walk,
4110 );
4111 })
4112 .is_none()
4113 {
4114 let char_width = fallback_char_width(font_size);
4115 let letter_spacing = resolve_letter_spacing(style, font_size);
4116 for _ in segment.chars() {
4117 walk.advance(0.0, letter_spacing + char_width);
4118 }
4119 }
4120}
4121
4122fn append_font_prefix_width_segment<S: PrefixWidthSink>(
4123 segment: &str,
4124 style: &TextStyle,
4125 font_size: f32,
4126 font: &SoftwareTextFont,
4127 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
4128 walk: &mut PrefixWidthWalk<'_, S>,
4129) {
4130 let shaped = font.shaped_for(style);
4131 let font = &*shaped;
4132 let glyph_font_size = font.ab_glyph_px_size(font_size);
4133 let scaled_font = font.font.as_scaled(PxScale::from(glyph_font_size));
4134 let letter_spacing = font.metadata.tracking.resolve(style, font_size);
4135 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, 1.0);
4136 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0);
4137 walk.non_empty_overhang = walk
4138 .non_empty_overhang
4139 .max(style_synthesis.visual_overhang_px());
4140
4141 let mut previous = None;
4142 let h_scale = scaled_font.h_scale_factor();
4143
4144 for (index, ch) in segment.chars().enumerate() {
4145 let metrics = glyph_metrics.glyph_metrics(font, &scaled_font, ch);
4146 let separator = if index == 0 {
4147 0.0
4148 } else {
4149 previous.map_or(0.0, |previous| {
4150 weight_synthesis.apply_width(
4151 glyph_metrics.kern(font, &scaled_font, previous, (ch, metrics.glyph_id))
4152 * h_scale,
4153 )
4154 })
4155 };
4156 walk.advance(
4157 separator,
4158 separator
4159 + letter_spacing
4160 + weight_synthesis.apply_width(metrics.advance_unscaled * h_scale),
4161 );
4162 previous = Some((ch, metrics.glyph_id));
4163 }
4164}
4165
4166fn byte_offset_for_char_index(text: &str, char_index: usize) -> usize {
4167 text.char_indices()
4168 .map(|(index, _)| index)
4169 .nth(char_index)
4170 .unwrap_or(text.len())
4171}
4172
4173fn measure_text_impl(
4174 text: &str,
4175 style: &TextStyle,
4176 font_size: f32,
4177 font_ref: RasterFontRef<'_, impl Font>,
4178) -> TextMetrics {
4179 let font = font_ref.font;
4180 let glyph_font_size = font_size * font_ref.ab_glyph_scale_factor;
4181 let line_box = line_box_for(
4182 style,
4183 vertical_metrics(font, glyph_font_size),
4184 asked_line_height(style, 1.0),
4185 measure_grid(),
4186 );
4187 let line_height = line_box.height;
4188 let letter_spacing = font_ref.tracking.resolve(style, font_size);
4189 let weight_synthesis = TextWeightSynthesis::for_style(style, font_ref.weight, font_size, 1.0);
4190 let style_synthesis = TextStyleSynthesis::for_style(style, font_ref.style, font_size, 1.0);
4191
4192 let mut max_width: f32 = 0.0;
4193 let mut line_count = 0usize;
4194 for line in text.split('\n') {
4195 line_count += 1;
4196 let line_width = line_advance_width(font, line, glyph_font_size);
4197 let char_spacing = run_tracking(line.chars().count(), letter_spacing);
4198 let line_width = (weight_synthesis.apply_width(line_width) + char_spacing).max(0.0);
4199 let line_width = if line.is_empty() {
4200 line_width
4201 } else {
4202 line_width + style_synthesis.visual_overhang_px()
4203 };
4204 max_width = max_width.max(line_width);
4205 }
4206
4207 TextMetrics {
4208 width: max_width,
4209 height: line_box.block_height(line_count),
4210 line_height,
4211 line_count,
4212 }
4213}
4214
4215fn measure_text_impl_cached(
4216 text: &str,
4217 style: &TextStyle,
4218 font_size: f32,
4219 font: &SoftwareTextFont,
4220 cache: &mut SoftwareTextMetricsCache,
4221) -> TextMetrics {
4222 let letter_spacing = font.metadata.tracking.resolve(style, font_size);
4223 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, 1.0);
4224 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0);
4225 let glyph_font_size = font.ab_glyph_px_size(font_size);
4226 let line_box = font_line_box(style, font, font_size);
4227 let line_height = line_box.height;
4228
4229 let mut max_width: f32 = 0.0;
4230 let mut line_count = 0usize;
4231 for line in text.split('\n') {
4232 line_count += 1;
4233 let line_width =
4234 cached_line_advance_width(font, line, glyph_font_size, &mut cache.glyph_metrics);
4235 let char_spacing = run_tracking(line.chars().count(), letter_spacing);
4236 let line_width = (weight_synthesis.apply_width(line_width) + char_spacing).max(0.0);
4237 let line_width = if line.is_empty() {
4238 line_width
4239 } else {
4240 line_width + style_synthesis.visual_overhang_px()
4241 };
4242 max_width = max_width.max(line_width);
4243 }
4244
4245 TextMetrics {
4246 width: max_width,
4247 height: line_box.block_height(line_count),
4248 line_height,
4249 line_count,
4250 }
4251}
4252
4253fn measure_annotated_text_with_resolver<'a>(
4254 text: impl Into<StyledTextRef<'a>>,
4255 style: &TextStyle,
4256 font_size: f32,
4257 fonts: impl Into<FontResolver<'a>>,
4258 cache: Option<&mut SoftwareTextMetricsCache>,
4259) -> TextMetrics {
4260 let text = text.into();
4261 let fonts = fonts.into();
4262 let Some(layout) = AnnotatedTextLayout::new(text, style, font_size, 1.0, measure_grid(), fonts)
4263 else {
4264 return fallback_text_metrics(text.text, style, font_size);
4265 };
4266 measure_resolved_text(&layout, cache)
4267}
4268
4269fn measure_resolved_text(
4270 layout: &AnnotatedTextLayout<'_>,
4271 mut cache: Option<&mut SoftwareTextMetricsCache>,
4272) -> TextMetrics {
4273 layout
4274 .walk(None, |segment| {
4275 #[cfg(feature = "text-shaping")]
4276 if requires_font_layout(segment.text, segment.style) {
4277 return Some(
4278 measure_shaped_text(
4279 segment.text,
4280 segment.style,
4281 segment.font_size,
4282 segment.font,
4283 )
4284 .width,
4285 );
4286 }
4287 let metrics = if let Some(cache) = cache.as_deref_mut() {
4288 measure_text_with_font_cached(
4289 segment.text,
4290 segment.style,
4291 segment.font_size,
4292 segment.font,
4293 cache,
4294 )
4295 } else {
4296 measure_text_with_font(segment.text, segment.style, segment.font_size, segment.font)
4297 };
4298 Some(metrics.width)
4299 })
4300 .unwrap_or_else(|| layout.metrics(0.0))
4301}
4302
4303fn max_line_height_for_annotated_text_with_resolver(
4304 text: &AnnotatedString,
4305 style: &TextStyle,
4306 font_size: f32,
4307 fonts: &SoftwareTextFontSet,
4308) -> f32 {
4309 if text.span_styles.is_empty()
4310 && let Some(font) = fonts.single_font_for_text(text.text.as_str(), style)
4311 {
4312 return font_line_box(style, font, font_size).height;
4313 }
4314 AnnotatedTextLayout::new(text.into(), style, font_size, 1.0, measure_grid(), fonts).map_or_else(
4315 || fallback_line_height(style, font_size),
4316 |layout| layout.metrics(0.0).line_height,
4317 )
4318}
4319
4320pub(crate) fn visit_annotated_line_boxes(
4321 text: &AnnotatedString,
4322 style: &TextStyle,
4323 fonts: &SoftwareTextFontSet,
4324 visit: &mut dyn FnMut(cranpose_ui::text::LineBox),
4325) -> Option<()> {
4326 let layout = AnnotatedTextLayout::new(
4327 text.into(),
4328 style,
4329 resolve_font_size(style),
4330 1.0,
4331 measure_grid(),
4332 fonts,
4333 )?;
4334 for line in &layout.lines {
4335 visit(line.line_box);
4336 }
4337 Some(())
4338}
4339
4340pub(crate) fn effective_style_for_range<'a>(
4341 span_styles: &[RangeStyle<SpanStyle>],
4342 style: &'a TextStyle,
4343 start: usize,
4344 end: usize,
4345) -> Cow<'a, TextStyle> {
4346 let mut effective = Cow::Borrowed(style);
4347 for span in span_styles {
4348 if span.range.start < end && span.range.end > start {
4349 let merged = effective.span_style.merge(&span.item);
4350 effective.to_mut().span_style = merged;
4351 }
4352 }
4353 effective
4354}
4355
4356fn clamp_to_char_boundary(text: &str, mut offset: usize) -> usize {
4357 offset = offset.min(text.len());
4358 while offset > 0 && !text.is_char_boundary(offset) {
4359 offset -= 1;
4360 }
4361 offset
4362}
4363
4364fn align_glyph_for_text_motion(glyph: Glyph, static_text_motion: bool) -> Glyph {
4365 align_glyph_to_pixel_grid(glyph, static_text_motion)
4366}
4367
4368fn static_glyph_pixel_origin(glyph: &Glyph) -> (i32, i32) {
4369 (
4370 glyph.position.x.round() as i32,
4371 glyph.position.y.round() as i32,
4372 )
4373}
4374
4375fn glyph_mask_cache_key(
4376 font_hash: u64,
4377 glyph: &Glyph,
4378 raster_style: GlyphRasterStyle,
4379 weight_synthesis: TextWeightSynthesis,
4380 style_synthesis: TextStyleSynthesis,
4381) -> GlyphMaskCacheKey {
4382 GlyphMaskCacheKey {
4383 font_hash,
4384 glyph_id: u32::from(glyph.id.0),
4385 scale_x_bits: glyph.scale.x.to_bits(),
4386 scale_y_bits: glyph.scale.y.to_bits(),
4387 raster_style: GlyphRasterStyleKey::from_style(raster_style),
4388 embolden_px_bits: weight_synthesis.embolden_px.to_bits(),
4389 slant_bits: style_synthesis.slant.to_bits(),
4390 }
4391}
4392
4393fn glyph_atlas_key_from_mask_key(key: GlyphMaskCacheKey) -> Option<SoftwareGlyphAtlasKey> {
4394 if !matches!(key.raster_style, GlyphRasterStyleKey::Fill) {
4395 return None;
4396 }
4397 Some(SoftwareGlyphAtlasKey {
4398 font_hash: key.font_hash,
4399 glyph_id: key.glyph_id,
4400 scale_x_bits: key.scale_x_bits,
4401 scale_y_bits: key.scale_y_bits,
4402 embolden_px_bits: key.embolden_px_bits,
4403 slant_bits: key.slant_bits,
4404 })
4405}
4406
4407fn build_complete_glyph_mask(
4408 font: &impl Font,
4409 glyph: &Glyph,
4410 raster_style: GlyphRasterStyle,
4411 weight_synthesis: TextWeightSynthesis,
4412 style_synthesis: TextStyleSynthesis,
4413 clip: Option<&GlyphRasterClip>,
4414) -> Option<GlyphMask> {
4415 let (outlined, bounds) = outline_glyph_with_bounds(font, glyph)?;
4416 if clip.is_some_and(|clip| {
4417 !clip.intersects(complete_glyph_bounds(
4418 bounds,
4419 raster_style,
4420 weight_synthesis,
4421 style_synthesis,
4422 ))
4423 }) {
4424 return None;
4425 }
4426 let mask = build_glyph_mask(font, glyph, &outlined, bounds, raster_style)?;
4427 let mask = synthesize_glyph_weight(mask, weight_synthesis);
4428 Some(synthesize_glyph_style(mask, style_synthesis))
4429}
4430
4431fn complete_glyph_bounds(
4432 bounds: GlyphPixelBounds,
4433 raster_style: GlyphRasterStyle,
4434 weight: TextWeightSynthesis,
4435 style: TextStyleSynthesis,
4436) -> Rect {
4437 let pad = match raster_style {
4438 GlyphRasterStyle::Fill => 0.0,
4439 GlyphRasterStyle::Stroke { width_px } => stroke_mask_padding(width_px) as f32,
4440 };
4441 let horizontal = synthetic_weight_shift_px(weight.embolden_px);
4442 let vertical = (horizontal / 2).min(1) as f32;
4443 let height = bounds.height() as f32 + 2.0 * (pad + vertical);
4444 let slant = if style.slant > 0.0 {
4445 ((height - 1.0).max(0.0) * style.slant).ceil() + 1.0
4446 } else {
4447 0.0
4448 };
4449 Rect {
4450 x: bounds.min_x as f32 - pad,
4451 y: bounds.min_y as f32 - pad - vertical,
4452 width: bounds.width() as f32 + 2.0 * pad + horizontal as f32 + slant,
4453 height,
4454 }
4455}
4456
4457#[expect(clippy::too_many_arguments)]
4458fn raster_glyph_mask(
4459 cache: Option<&mut SoftwareGlyphRasterCache>,
4460 font_hash: u64,
4461 font: &impl Font,
4462 glyph: &Glyph,
4463 raster_style: GlyphRasterStyle,
4464 weight_synthesis: TextWeightSynthesis,
4465 style_synthesis: TextStyleSynthesis,
4466 clip: Option<&GlyphRasterClip>,
4467) -> Option<GlyphMask> {
4468 if let Some(cache) = cache {
4469 let key = glyph_mask_cache_key(
4470 font_hash,
4471 glyph,
4472 raster_style,
4473 weight_synthesis,
4474 style_synthesis,
4475 );
4476 if let Some(mask) = cache.get(&key, glyph) {
4477 return clip
4478 .is_none_or(|clip| {
4479 clip.intersects(Rect {
4480 x: mask.origin_x as f32,
4481 y: mask.origin_y as f32,
4482 width: mask.width as f32,
4483 height: mask.height as f32,
4484 })
4485 })
4486 .then_some(mask);
4487 }
4488 let mask = build_complete_glyph_mask(
4489 font,
4490 glyph,
4491 raster_style,
4492 weight_synthesis,
4493 style_synthesis,
4494 clip,
4495 )?;
4496 return Some(cache.put(key, glyph, mask));
4497 }
4498 build_complete_glyph_mask(
4499 font,
4500 glyph,
4501 raster_style,
4502 weight_synthesis,
4503 style_synthesis,
4504 clip,
4505 )
4506}
4507
4508fn line_alignment_offsets<F: Font, S: ScaleFont<F>>(
4509 scaled_font: &S,
4510 text: &str,
4511 letter_spacing: f32,
4512 align_fraction: f32,
4513) -> Option<Vec<f32>> {
4514 if align_fraction == 0.0 || !text.contains('\n') {
4515 return None;
4516 }
4517 let advances: Vec<f32> = text
4518 .split('\n')
4519 .map(|line| {
4520 let mut advance = 0.0f32;
4521 let mut previous = None;
4522 for ch in line.chars() {
4523 let glyph_id = scaled_font.glyph_id(ch);
4524 if let Some(previous_id) = previous {
4525 advance += scaled_font.kern(previous_id, glyph_id);
4526 }
4527 advance += letter_spacing + scaled_font.h_advance(glyph_id);
4528 previous = Some(glyph_id);
4529 }
4530 advance.max(0.0)
4531 })
4532 .collect();
4533 let block = advances.iter().copied().fold(0.0f32, f32::max);
4534 Some(
4535 advances
4536 .iter()
4537 .map(|advance| ((block - advance) * align_fraction).max(0.0))
4538 .collect(),
4539 )
4540}
4541
4542fn line_offset(offsets: &Option<Vec<f32>>, line_idx: usize) -> f32 {
4543 offsets
4544 .as_ref()
4545 .and_then(|offsets| offsets.get(line_idx).copied())
4546 .unwrap_or(0.0)
4547}
4548
4549fn cached_kern<F: Font, S: ScaleFont<F>>(
4552 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
4553 font_hash: u64,
4554 scaled_font: &S,
4555 previous: GlyphId,
4556 glyph: GlyphId,
4557) -> f32 {
4558 match glyph_cache {
4559 Some(cache) => {
4560 cache.kern_unscaled(font_hash, scaled_font.font(), previous, glyph)
4561 * scaled_font.h_scale_factor()
4562 }
4563 None => scaled_font.kern(previous, glyph),
4564 }
4565}
4566
4567#[expect(clippy::too_many_arguments)]
4568fn visit_text_glyph_masks(
4569 text: &str,
4570 font: &impl Font,
4571 font_hash: u64,
4572 font_px_size: f32,
4573 line_height: f32,
4574 first_baseline_y: f32,
4575 origin_x: f32,
4576 origin_y: f32,
4577 letter_spacing: f32,
4578 align_fraction: f32,
4579 static_text_motion: bool,
4580 raster_style: GlyphRasterStyle,
4581 weight_synthesis: TextWeightSynthesis,
4582 style_synthesis: TextStyleSynthesis,
4583 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
4584 clip: Option<&GlyphRasterClip>,
4585 mut visit: impl FnMut(&GlyphMask),
4586) -> f32 {
4587 let scale = PxScale::from(font_px_size);
4588 let scaled_font = font.as_scaled(scale);
4589 let line_offsets = line_alignment_offsets(&scaled_font, text, letter_spacing, align_fraction);
4590 let mut max_advance = 0.0f32;
4591 for (line_idx, line) in text.split('\n').enumerate() {
4592 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
4593 let lead_in = run_lead_in(line, letter_spacing);
4594 let mut caret_x = origin_x + line_offset(&line_offsets, line_idx) + lead_in;
4595 let mut previous = None;
4596 for ch in line.chars() {
4597 let glyph_id = scaled_font.glyph_id(ch);
4598 if let Some(previous_id) = previous {
4599 caret_x += cached_kern(
4600 glyph_cache.as_deref_mut(),
4601 font_hash,
4602 &scaled_font,
4603 previous_id,
4604 glyph_id,
4605 ) + letter_spacing;
4606 }
4607 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
4608 caret_x += scaled_font.h_advance(glyph_id);
4609 previous = Some(glyph_id);
4610 let glyph = align_glyph_for_text_motion(glyph, static_text_motion);
4611 let Some(mask) = raster_glyph_mask(
4612 if static_text_motion {
4613 glyph_cache.as_deref_mut()
4614 } else {
4615 None
4616 },
4617 font_hash,
4618 font,
4619 &glyph,
4620 raster_style,
4621 weight_synthesis,
4622 style_synthesis,
4623 clip,
4624 ) else {
4625 continue;
4626 };
4627 visit(&mask);
4628 }
4629 max_advance = max_advance.max((caret_x - origin_x + lead_in).max(0.0));
4630 }
4631 max_advance
4632}
4633
4634fn blend_src_over(dst: &mut [f32; 4], src: [f32; 4]) {
4635 let src_alpha = src[3].clamp(0.0, 1.0);
4636 if src_alpha <= 0.0 {
4637 return;
4638 }
4639
4640 let dst_alpha = dst[3].clamp(0.0, 1.0);
4641 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
4642
4643 if out_alpha <= f32::EPSILON {
4644 *dst = [0.0, 0.0, 0.0, 0.0];
4645 return;
4646 }
4647
4648 for channel in 0..3 {
4649 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
4650 let dst_premult = dst[channel].clamp(0.0, 1.0) * dst_alpha;
4651 dst[channel] =
4652 ((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0);
4653 }
4654 dst[3] = out_alpha;
4655}
4656
4657fn draw_mask_glyph(
4658 canvas: &mut [[f32; 4]],
4659 width: u32,
4660 height: u32,
4661 mask: &GlyphMask,
4662 paint: &GlyphPaint<'_>,
4663) {
4664 let correction = TextLuminance::of_brush(paint.brush).correction();
4665 for y in 0..mask.height {
4666 let py = mask.origin_y + y as i32;
4667 if py < 0 || py >= height as i32 {
4668 continue;
4669 }
4670
4671 for x in 0..mask.width {
4672 let px = mask.origin_x + x as i32;
4673 if px < 0 || px >= width as i32 {
4674 continue;
4675 }
4676
4677 let coverage = correction.apply_unit(mask.alpha[y * mask.width + x]);
4678 if coverage <= 0.0 {
4679 continue;
4680 }
4681
4682 let sample = sample_brush_rgba(
4683 paint.brush,
4684 paint.brush_rect,
4685 paint.canvas_origin.x + px as f32 + 0.5,
4686 paint.canvas_origin.y + py as f32 + 0.5,
4687 cranpose_ui_graphics::Point::default(),
4688 );
4689 let alpha = coverage * sample[3] * paint.alpha;
4690 if alpha <= 0.0 {
4691 continue;
4692 }
4693 let idx = (py as u32 * width + px as u32) as usize;
4694 blend_src_over(
4695 &mut canvas[idx],
4696 [sample[0], sample[1], sample[2], alpha.clamp(0.0, 1.0)],
4697 );
4698 }
4699 }
4700}
4701
4702fn blend_src_over_u8(dst: &mut [u8], src: [f32; 4]) {
4703 let src_alpha = src[3].clamp(0.0, 1.0);
4704 if src_alpha <= 0.0 {
4705 return;
4706 }
4707
4708 let dst_alpha = dst[3] as f32 / 255.0;
4709 if dst_alpha <= 0.0 {
4710 dst[0] = (src[0].clamp(0.0, 1.0) * 255.0).round() as u8;
4711 dst[1] = (src[1].clamp(0.0, 1.0) * 255.0).round() as u8;
4712 dst[2] = (src[2].clamp(0.0, 1.0) * 255.0).round() as u8;
4713 dst[3] = (src_alpha * 255.0).round() as u8;
4714 return;
4715 }
4716
4717 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
4718 if out_alpha <= f32::EPSILON {
4719 dst.fill(0);
4720 return;
4721 }
4722
4723 for channel in 0..3 {
4724 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
4725 let dst_premult = (dst[channel] as f32 / 255.0) * dst_alpha;
4726 dst[channel] =
4727 ((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha * 255.0).round() as u8;
4728 }
4729 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
4730}
4731
4732fn draw_mask_glyph_solid_u8(
4733 canvas: &mut [u8],
4734 width: u32,
4735 height: u32,
4736 mask: &GlyphMask,
4737 color: [f32; 4],
4738 alpha_multiplier: f32,
4739) {
4740 let red = (color[0].clamp(0.0, 1.0) * 255.0).round() as u8;
4741 let green = (color[1].clamp(0.0, 1.0) * 255.0).round() as u8;
4742 let blue = (color[2].clamp(0.0, 1.0) * 255.0).round() as u8;
4743 let alpha_scale = color[3].clamp(0.0, 1.0) * alpha_multiplier.clamp(0.0, 1.0);
4744 if alpha_scale <= 0.0 {
4745 return;
4746 }
4747 let correction = TextLuminance::of_color(Color(color[0], color[1], color[2], 1.0)).correction();
4748
4749 for y in 0..mask.height {
4750 let py = mask.origin_y + y as i32;
4751 if py < 0 || py >= height as i32 {
4752 continue;
4753 }
4754
4755 for x in 0..mask.width {
4756 let px = mask.origin_x + x as i32;
4757 if px < 0 || px >= width as i32 {
4758 continue;
4759 }
4760
4761 let coverage = correction.apply_unit(mask.alpha[y * mask.width + x]);
4762 if coverage <= 0.0 {
4763 continue;
4764 }
4765
4766 let alpha = (coverage * alpha_scale).clamp(0.0, 1.0);
4767 let alpha_u8 = (alpha * 255.0).round() as u8;
4768 if alpha_u8 == 0 {
4769 continue;
4770 }
4771 let idx = ((py as u32 * width + px as u32) * 4) as usize;
4772 let dst = &mut canvas[idx..idx + 4];
4773 if dst[3] == 0 {
4774 dst[0] = red;
4775 dst[1] = green;
4776 dst[2] = blue;
4777 dst[3] = alpha_u8;
4778 } else {
4779 blend_src_over_u8(dst, [color[0], color[1], color[2], alpha]);
4780 }
4781 }
4782 }
4783}
4784
4785fn draw_shadow_mask(
4786 canvas: &mut [[f32; 4]],
4787 width: u32,
4788 height: u32,
4789 mask: &GlyphMask,
4790 shadow: Shadow,
4791 text_scale: f32,
4792 static_text_motion: bool,
4793) {
4794 if mask.width == 0 || mask.height == 0 {
4795 return;
4796 }
4797
4798 let shadow_dx = shadow.offset.x * text_scale;
4799 let shadow_dy = shadow.offset.y * text_scale;
4800 let blur_radius = (shadow.blur_radius * text_scale).max(0.0);
4801 let sigma = shadow_blur_sigma(blur_radius);
4802 let blur_margin = if sigma > 0.0 {
4803 (sigma * 3.0).ceil() as i32
4804 } else {
4805 0
4806 };
4807
4808 let padded_width = mask.width + (blur_margin as usize) * 2;
4809 let padded_height = mask.height + (blur_margin as usize) * 2;
4810 let mut padded_mask = vec![0.0f32; padded_width * padded_height];
4811
4812 let correction = (sigma <= 0.0).then(|| TextLuminance::of_color(shadow.color).correction());
4815 for y in 0..mask.height {
4816 let src_offset = y * mask.width;
4817 let dst_offset = (y + blur_margin as usize) * padded_width + blur_margin as usize;
4818 let source = &mask.alpha[src_offset..src_offset + mask.width];
4819 let target = &mut padded_mask[dst_offset..dst_offset + mask.width];
4820 match correction {
4821 Some(correction) => target
4822 .iter_mut()
4823 .zip(source)
4824 .for_each(|(target, &coverage)| *target = correction.apply_unit(coverage)),
4825 None => target.copy_from_slice(source),
4826 }
4827 }
4828
4829 let blurred = if sigma > 0.0 {
4830 gaussian_blur_alpha(&padded_mask, padded_width, padded_height, sigma)
4831 } else {
4832 padded_mask
4833 };
4834
4835 let shadow_rgba = color_to_rgba(shadow.color);
4836 let shadow_origin_x = mask.origin_x - blur_margin;
4837 let shadow_origin_y = mask.origin_y - blur_margin;
4838
4839 for y in 0..padded_height {
4840 for x in 0..padded_width {
4841 let alpha = blurred[y * padded_width + x] * shadow_rgba[3];
4842 if alpha <= 0.0 {
4843 continue;
4844 }
4845
4846 let target_x = shadow_origin_x as f32 + x as f32 + shadow_dx;
4847 let target_y = shadow_origin_y as f32 + y as f32 + shadow_dy;
4848 if static_text_motion {
4849 blend_shadow_pixel(
4850 canvas,
4851 width,
4852 height,
4853 target_x.round() as i32,
4854 target_y.round() as i32,
4855 shadow_rgba,
4856 alpha.clamp(0.0, 1.0),
4857 );
4858 } else {
4859 blend_shadow_pixel_subpixel(
4860 canvas,
4861 width,
4862 height,
4863 target_x,
4864 target_y,
4865 shadow_rgba,
4866 alpha.clamp(0.0, 1.0),
4867 );
4868 }
4869 }
4870 }
4871}
4872
4873fn blend_shadow_pixel(
4874 canvas: &mut [[f32; 4]],
4875 width: u32,
4876 height: u32,
4877 px: i32,
4878 py: i32,
4879 color: [f32; 4],
4880 alpha: f32,
4881) {
4882 if px < 0 || py < 0 || px >= width as i32 || py >= height as i32 || alpha <= 0.0 {
4883 return;
4884 }
4885 let idx = (py as u32 * width + px as u32) as usize;
4886 blend_src_over(
4887 &mut canvas[idx],
4888 [color[0], color[1], color[2], alpha.clamp(0.0, 1.0)],
4889 );
4890}
4891
4892fn blend_shadow_pixel_subpixel(
4893 canvas: &mut [[f32; 4]],
4894 width: u32,
4895 height: u32,
4896 x: f32,
4897 y: f32,
4898 color: [f32; 4],
4899 alpha: f32,
4900) {
4901 if alpha <= 0.0 {
4902 return;
4903 }
4904
4905 let base_x = x.floor();
4906 let base_y = y.floor();
4907 let frac_x = x - base_x;
4908 let frac_y = y - base_y;
4909 let base_x_i32 = base_x as i32;
4910 let base_y_i32 = base_y as i32;
4911 let weights = [
4912 ((1.0 - frac_x) * (1.0 - frac_y), 0i32, 0i32),
4913 (frac_x * (1.0 - frac_y), 1, 0),
4914 ((1.0 - frac_x) * frac_y, 0, 1),
4915 (frac_x * frac_y, 1, 1),
4916 ];
4917
4918 for (weight, dx, dy) in weights {
4919 if weight <= 0.0 {
4920 continue;
4921 }
4922 blend_shadow_pixel(
4923 canvas,
4924 width,
4925 height,
4926 base_x_i32 + dx,
4927 base_y_i32 + dy,
4928 color,
4929 alpha * weight,
4930 );
4931 }
4932}
4933
4934fn shadow_blur_sigma(blur_radius: f32) -> f32 {
4935 if blur_radius <= 0.0 {
4936 0.0
4937 } else {
4938 (blur_radius * SHADOW_SIGMA_SCALE + SHADOW_SIGMA_BIAS).max(0.5)
4939 }
4940}
4941
4942fn gaussian_blur_alpha(src: &[f32], width: usize, height: usize, sigma: f32) -> Vec<f32> {
4943 let kernel = gaussian_kernel_1d(sigma);
4944 if kernel.len() == 1 {
4945 return src.to_vec();
4946 }
4947 let half = (kernel.len() / 2) as i32;
4948
4949 let mut horizontal = vec![0.0f32; src.len()];
4950 for y in 0..height {
4951 for x in 0..width {
4952 let mut sum = 0.0f32;
4953 for (index, weight) in kernel.iter().enumerate() {
4954 let offset = index as i32 - half;
4955 let sample_x = (x as i32 + offset).clamp(0, width as i32 - 1) as usize;
4956 sum += src[y * width + sample_x] * *weight;
4957 }
4958 horizontal[y * width + x] = sum;
4959 }
4960 }
4961
4962 let mut output = vec![0.0f32; src.len()];
4963 for y in 0..height {
4964 for x in 0..width {
4965 let mut sum = 0.0f32;
4966 for (index, weight) in kernel.iter().enumerate() {
4967 let offset = index as i32 - half;
4968 let sample_y = (y as i32 + offset).clamp(0, height as i32 - 1) as usize;
4969 sum += horizontal[sample_y * width + x] * *weight;
4970 }
4971 output[y * width + x] = sum;
4972 }
4973 }
4974
4975 output
4976}
4977
4978fn gaussian_kernel_1d(sigma: f32) -> Vec<f32> {
4979 let half = ((sigma * 3.0).ceil() as i32).clamp(1, MAX_GAUSSIAN_KERNEL_HALF);
4980 if half <= 0 {
4981 return vec![1.0];
4982 }
4983
4984 let mut kernel = Vec::with_capacity((half * 2 + 1) as usize);
4985 let mut sum = 0.0f32;
4986 for offset in -half..=half {
4987 let distance = offset as f32;
4988 let weight = (-0.5 * (distance / sigma).powi(2)).exp();
4989 kernel.push(weight);
4990 sum += weight;
4991 }
4992
4993 if sum > f32::EPSILON {
4994 for weight in &mut kernel {
4995 *weight /= sum;
4996 }
4997 }
4998
4999 kernel
5000}
5001
5002fn outline_glyph_with_bounds(
5003 font: &impl Font,
5004 glyph: &Glyph,
5005) -> Option<(OutlinedGlyph, GlyphPixelBounds)> {
5006 let outlined = font.outline_glyph(glyph.clone())?;
5007 let bounds = pixel_bounds_from_outlined(&outlined);
5008 Some((outlined, bounds))
5009}
5010
5011fn build_glyph_mask(
5012 font: &impl Font,
5013 glyph: &Glyph,
5014 outlined: &OutlinedGlyph,
5015 bounds: GlyphPixelBounds,
5016 style: GlyphRasterStyle,
5017) -> Option<GlyphMask> {
5018 match style {
5019 GlyphRasterStyle::Fill => build_fill_mask(outlined, bounds),
5020 GlyphRasterStyle::Stroke { width_px } => {
5021 build_stroke_mask(font, glyph, outlined, bounds, width_px)
5022 }
5023 }
5024}
5025
5026fn build_fill_mask(outlined: &OutlinedGlyph, bounds: GlyphPixelBounds) -> Option<GlyphMask> {
5027 let mask_width = bounds.width();
5028 let mask_height = bounds.height();
5029 if mask_width == 0 || mask_height == 0 {
5030 return None;
5031 }
5032
5033 let mut alpha = vec![0.0f32; mask_width * mask_height];
5034 outlined.draw(|gx, gy, value| {
5035 let idx = gy as usize * mask_width + gx as usize;
5036 alpha[idx] = value;
5037 });
5038
5039 Some(GlyphMask {
5040 alpha: Arc::from(alpha),
5041 width: mask_width,
5042 height: mask_height,
5043 origin_x: bounds.min_x,
5044 origin_y: bounds.min_y,
5045 })
5046}
5047
5048fn build_stroke_mask(
5049 font: &impl Font,
5050 glyph: &Glyph,
5051 outlined: &OutlinedGlyph,
5052 bounds: GlyphPixelBounds,
5053 stroke_width_px: f32,
5054) -> Option<GlyphMask> {
5055 if !stroke_width_px.is_finite() || stroke_width_px <= 0.0 {
5056 return build_fill_mask(outlined, bounds);
5057 }
5058
5059 let mask_width = bounds.max_x - bounds.min_x;
5060 let mask_height = bounds.max_y - bounds.min_y;
5061 if mask_width <= 0 || mask_height <= 0 {
5062 return None;
5063 }
5064
5065 let pad = stroke_mask_padding(stroke_width_px);
5066 let path = build_outline_path(font, glyph, bounds, pad)?;
5067 let raster_width = mask_width + pad * 2;
5068 let raster_height = mask_height + pad * 2;
5069 if raster_width <= 0 || raster_height <= 0 {
5070 return None;
5071 }
5072
5073 let mut pixmap = Pixmap::new(raster_width as u32, raster_height as u32)?;
5074 let mut paint = Paint::default();
5075 paint.set_color_rgba8(255, 255, 255, 255);
5076 paint.anti_alias = true;
5077
5078 let stroke = Stroke {
5079 width: stroke_width_px,
5080 line_cap: LineCap::Butt,
5081 line_join: LineJoin::Miter,
5082 miter_limit: COMPOSE_STROKE_MITER_LIMIT,
5083 ..Stroke::default()
5084 };
5085
5086 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
5087
5088 let alpha: Vec<f32> = pixmap
5089 .data()
5090 .as_chunks::<4>()
5091 .0
5092 .iter()
5093 .map(|pixel| pixel[3] as f32 / 255.0)
5094 .collect();
5095
5096 Some(GlyphMask {
5097 alpha: Arc::from(alpha),
5098 width: raster_width as usize,
5099 height: raster_height as usize,
5100 origin_x: bounds.min_x - pad,
5101 origin_y: bounds.min_y - pad,
5102 })
5103}
5104
5105fn stroke_mask_padding(width: f32) -> i32 {
5106 if width.is_finite() && width > 0.0 {
5107 (width * 0.5 * COMPOSE_STROKE_MITER_LIMIT).ceil().max(1.0) as i32 + 1
5108 } else {
5109 0
5110 }
5111}
5112
5113fn synthesize_glyph_weight(mask: GlyphMask, synthesis: TextWeightSynthesis) -> GlyphMask {
5114 let horizontal_shift = synthetic_weight_shift_px(synthesis.embolden_px);
5115 if horizontal_shift == 0 || mask.width == 0 || mask.height == 0 {
5116 return mask;
5117 }
5118
5119 let vertical_shift = (horizontal_shift / 2).min(1);
5120 let output_width = mask.width + horizontal_shift;
5121 let output_height = mask.height + vertical_shift * 2;
5122 let mut alpha = vec![0.0f32; output_width * output_height];
5123 for y in 0..mask.height {
5124 for x in 0..mask.width {
5125 let coverage = mask.alpha[y * mask.width + x];
5126 if coverage <= 0.0 {
5127 continue;
5128 }
5129 for dy in 0..=(vertical_shift * 2) {
5130 let output_y = y + dy;
5131 for dx in 0..=horizontal_shift {
5132 let output_x = x + dx;
5133 let output_index = output_y * output_width + output_x;
5134 if coverage > alpha[output_index] {
5135 alpha[output_index] = coverage;
5136 }
5137 }
5138 }
5139 }
5140 }
5141
5142 GlyphMask {
5143 alpha: Arc::from(alpha),
5144 width: output_width,
5145 height: output_height,
5146 origin_x: mask.origin_x,
5147 origin_y: mask.origin_y - vertical_shift as i32,
5148 }
5149}
5150
5151fn synthesize_glyph_style(mask: GlyphMask, synthesis: TextStyleSynthesis) -> GlyphMask {
5152 if synthesis.slant <= 0.0 || mask.width == 0 || mask.height == 0 {
5153 return mask;
5154 }
5155
5156 let max_shift = ((mask.height.saturating_sub(1)) as f32 * synthesis.slant).ceil() as usize;
5157 if max_shift == 0 {
5158 return mask;
5159 }
5160
5161 let output_width = mask.width + max_shift + 1;
5162 let mut alpha = vec![0.0f32; output_width * mask.height];
5163 for y in 0..mask.height {
5164 let shift = (mask.height.saturating_sub(1) - y) as f32 * synthesis.slant;
5165 let shift_floor = shift.floor() as usize;
5166 let shift_fraction = shift - shift.floor();
5167 for x in 0..mask.width {
5168 let coverage = mask.alpha[y * mask.width + x];
5169 if coverage <= 0.0 {
5170 continue;
5171 }
5172
5173 let output_x = x + shift_floor;
5174 let left_index = y * output_width + output_x;
5175 let left_coverage = coverage * (1.0 - shift_fraction);
5176 if left_coverage > alpha[left_index] {
5177 alpha[left_index] = left_coverage;
5178 }
5179
5180 if shift_fraction > 0.0 {
5181 let right_index = left_index + 1;
5182 let right_coverage = coverage * shift_fraction;
5183 if right_coverage > alpha[right_index] {
5184 alpha[right_index] = right_coverage;
5185 }
5186 }
5187 }
5188 }
5189
5190 GlyphMask {
5191 alpha: Arc::from(alpha),
5192 width: output_width,
5193 height: mask.height,
5194 origin_x: mask.origin_x,
5195 origin_y: mask.origin_y,
5196 }
5197}
5198
5199fn synthetic_weight_shift_px(embolden_px: f32) -> usize {
5200 if !embolden_px.is_finite() || embolden_px < 0.35 {
5201 return 0;
5202 }
5203 embolden_px.ceil().max(1.0) as usize
5204}
5205
5206fn build_outline_path(
5207 font: &impl Font,
5208 glyph: &Glyph,
5209 bounds: GlyphPixelBounds,
5210 pad: i32,
5211) -> Option<Path> {
5212 let outline = font.outline(glyph.id)?;
5213 let scale_factor = font.as_scaled(glyph.scale).scale_factor();
5214 let mut builder = PathBuilder::new();
5215 let mut has_segments = false;
5216 let mut current_end = None;
5217 let mut subpath_start = None;
5218
5219 for curve in outline.curves {
5220 match curve {
5221 ab_glyph::OutlineCurve::Line(p0, p1) => {
5222 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
5223 let end = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
5224 if current_end != Some(start) {
5225 if current_end.is_some() {
5226 builder.close();
5227 }
5228 builder.move_to(start.0, start.1);
5229 subpath_start = Some(start);
5230 }
5231 builder.line_to(end.0, end.1);
5232 if subpath_start == Some(end) {
5233 builder.close();
5234 current_end = None;
5235 subpath_start = None;
5236 } else {
5237 current_end = Some(end);
5238 }
5239 }
5240 ab_glyph::OutlineCurve::Quad(p0, p1, p2) => {
5241 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
5242 let control = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
5243 let end = transform_outline_point(p2, scale_factor, glyph, bounds, pad);
5244 if current_end != Some(start) {
5245 if current_end.is_some() {
5246 builder.close();
5247 }
5248 builder.move_to(start.0, start.1);
5249 subpath_start = Some(start);
5250 }
5251 builder.quad_to(control.0, control.1, end.0, end.1);
5252 if subpath_start == Some(end) {
5253 builder.close();
5254 current_end = None;
5255 subpath_start = None;
5256 } else {
5257 current_end = Some(end);
5258 }
5259 }
5260 ab_glyph::OutlineCurve::Cubic(p0, p1, p2, p3) => {
5261 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
5262 let control1 = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
5263 let control2 = transform_outline_point(p2, scale_factor, glyph, bounds, pad);
5264 let end = transform_outline_point(p3, scale_factor, glyph, bounds, pad);
5265 if current_end != Some(start) {
5266 if current_end.is_some() {
5267 builder.close();
5268 }
5269 builder.move_to(start.0, start.1);
5270 subpath_start = Some(start);
5271 }
5272 builder.cubic_to(control1.0, control1.1, control2.0, control2.1, end.0, end.1);
5273 if subpath_start == Some(end) {
5274 builder.close();
5275 current_end = None;
5276 subpath_start = None;
5277 } else {
5278 current_end = Some(end);
5279 }
5280 }
5281 }
5282 has_segments = true;
5283 }
5284
5285 if !has_segments {
5286 return None;
5287 }
5288
5289 if current_end.is_some() {
5290 builder.close();
5291 }
5292
5293 builder.finish()
5294}
5295
5296fn transform_outline_point(
5297 point: ab_glyph::Point,
5298 scale_factor: ab_glyph::PxScaleFactor,
5299 glyph: &Glyph,
5300 bounds: GlyphPixelBounds,
5301 pad: i32,
5302) -> (f32, f32) {
5303 (
5304 point.x * scale_factor.horizontal + glyph.position.x - bounds.min_x as f32 + pad as f32,
5305 point.y * -scale_factor.vertical + glyph.position.y - bounds.min_y as f32 + pad as f32,
5306 )
5307}
5308
5309#[cfg(test)]
5310#[path = "tests/software_text_raster_tests.rs"]
5311mod tests;
5312
5313#[cfg(test)]
5314#[path = "tests/software_text_raster_line_alignment_tests.rs"]
5315mod line_alignment_tests;
5316
5317#[cfg(test)]
5318#[path = "tests/software_text_features_tests.rs"]
5319mod feature_tests;