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