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