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