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