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