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