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 prefix_widths: PassAgedCache<TextCacheKey<TextMetricsParams>, Option<TextLinePrefixWidths>>,
930 line_widths: PassAgedCache<TextCacheKey<TextMetricsParams>, Option<f32>>,
931 glyph_metrics: SoftwareTextGlyphMetricsCache,
932}
933
934impl SoftwareTextMetricsCache {
935 fn new(capacity: usize) -> Self {
936 Self {
937 map: PassAgedCache::with_capacity_at_least_one(capacity),
938 prefix_widths: PassAgedCache::with_capacity_at_least_one(capacity),
939 line_widths: PassAgedCache::with_capacity_at_least_one(capacity),
940 glyph_metrics: SoftwareTextGlyphMetricsCache::new(),
941 }
942 }
943
944 fn get_or_measure(
945 &mut self,
946 fonts: &SoftwareTextFontSet,
947 text: &AnnotatedString,
948 style: &TextStyle,
949 ) -> TextMetrics {
950 self.get_or_measure_range(fonts, text, 0..text.text.len(), style)
951 }
952
953 fn get_or_measure_range(
954 &mut self,
955 fonts: &SoftwareTextFontSet,
956 text: &AnnotatedString,
957 range: std::ops::Range<usize>,
958 style: &TextStyle,
959 ) -> TextMetrics {
960 let font_size = resolve_font_size(style);
961 let probe = range_probe(text, &range, style);
962 if let Some(metrics) = self.map.get(probe.key()).copied() {
963 return metrics;
964 }
965
966 let metrics = if range == (0..text.text.len()) {
967 measure_annotated_text_with_font_set_cached(text, style, font_size, fonts, self)
968 } else {
969 let layout = AnnotatedTextLayout::new_range(
970 text.into(),
971 range.clone(),
972 style,
973 font_size,
974 1.0,
975 measure_grid(),
976 fonts,
977 );
978 layout.map_or_else(
979 || fallback_text_metrics(&text.text[range], style, font_size),
980 |layout| measure_resolved_text(&layout, Some(self)),
981 )
982 };
983 self.map.push(probe.to_owned_key(), metrics);
984 metrics
985 }
986
987 fn get_or_measure_prefix_widths(
990 &mut self,
991 fonts: &SoftwareTextFontSet,
992 text: &AnnotatedString,
993 line_range: std::ops::Range<usize>,
994 style: &TextStyle,
995 ) -> Option<TextLinePrefixWidths> {
996 let probe = range_probe(text, &line_range, style);
997 if let Some(widths) = self.prefix_widths.get(probe.key()) {
998 return widths.clone();
999 }
1000 let char_count = text.text[line_range.clone()].chars().count();
1001 let mut sink = PrefixWidthVecs {
1002 prefix_widths: Vec::with_capacity(char_count + 1),
1003 separator_before: Vec::with_capacity(char_count),
1004 };
1005 sink.prefix_widths.push(0.0);
1006 let overhang = walk_line_prefix_widths(
1007 text,
1008 line_range,
1009 style,
1010 fonts,
1011 &mut self.glyph_metrics,
1012 &mut sink,
1013 );
1014 let widths =
1015 TextLinePrefixWidths::from_parts(sink.prefix_widths, sink.separator_before, overhang);
1016 self.prefix_widths
1017 .push(probe.to_owned_key(), widths.clone());
1018 widths
1019 }
1020
1021 fn get_or_measure_line_width(
1025 &mut self,
1026 fonts: &SoftwareTextFontSet,
1027 text: &AnnotatedString,
1028 line_range: std::ops::Range<usize>,
1029 style: &TextStyle,
1030 ) -> Option<f32> {
1031 let probe = range_probe(text, &line_range, style);
1032 if let Some(width) = self.line_widths.get(probe.key()) {
1033 return *width;
1034 }
1035 let width = match self.prefix_widths.get(probe.key()) {
1036 Some(widths) => widths
1037 .as_ref()
1038 .and_then(|widths| widths.width_for_char_range(0, widths.char_count())),
1039 None => {
1040 let mut sink = LineWidthSink::default();
1041 let overhang = walk_line_prefix_widths(
1042 text,
1043 line_range,
1044 style,
1045 fonts,
1046 &mut self.glyph_metrics,
1047 &mut sink,
1048 );
1049 sink.width(overhang)
1050 }
1051 };
1052 self.line_widths.push(probe.to_owned_key(), width);
1053 width
1054 }
1055
1056 fn begin_layout_pass(&mut self) {
1058 self.map.begin_pass(|_| {});
1059 self.prefix_widths.begin_pass(|_| {});
1060 self.line_widths.begin_pass(|_| {});
1061 }
1062}
1063
1064fn range_probe<'a>(
1067 text: &'a AnnotatedString,
1068 range: &std::ops::Range<usize>,
1069 style: &TextStyle,
1070) -> TextProbe<'a, TextMetricsParams> {
1071 TextProbe::new(
1072 &text.text[range.clone()],
1073 (
1074 resolve_font_size(style).to_bits(),
1075 style.measurement_hash(),
1076 text.span_measurement_hash(),
1077 range.start,
1078 ),
1079 )
1080}
1081
1082fn measures_line_prefixes(
1086 text: &AnnotatedString,
1087 line_range: &std::ops::Range<usize>,
1088 style: &TextStyle,
1089) -> bool {
1090 !requires_font_layout(&text.text, style)
1091 && style_allows_prefix_widths(style)
1092 && is_single_line_range(text, line_range)
1093}
1094
1095fn is_single_line_range(text: &AnnotatedString, line_range: &std::ops::Range<usize>) -> bool {
1096 line_range.start <= line_range.end
1097 && line_range.end <= text.text.len()
1098 && text.text.is_char_boundary(line_range.start)
1099 && text.text.is_char_boundary(line_range.end)
1100 && !text.text[line_range.clone()].contains('\n')
1101}
1102
1103pub(crate) fn requires_font_layout(text: &str, style: &TextStyle) -> bool {
1104 #[cfg(feature = "text-shaping")]
1105 {
1106 crate::text_shaping::required(text, style)
1107 }
1108 #[cfg(not(feature = "text-shaping"))]
1109 {
1110 let _ = (text, style);
1111 false
1112 }
1113}
1114
1115#[derive(Clone, Copy, Debug)]
1116struct CachedGlyphMetrics {
1117 glyph_id: GlyphId,
1118 advance_unscaled: f32,
1119}
1120
1121#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1122struct GlyphMetricsKey {
1123 font_hash: u64,
1124 ch: char,
1125}
1126
1127#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1128struct KernMetricsKey {
1129 font_hash: u64,
1130 previous_id: u32,
1131 glyph_id: u32,
1132}
1133
1134#[cfg(test)]
1135#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1136struct SoftwareTextGlyphMetricsStats {
1137 glyph_hits: u64,
1138 glyph_misses: u64,
1139 kern_hits: u64,
1140 kern_misses: u64,
1141}
1142
1143#[derive(Clone, Copy)]
1145enum MetricsLookup {
1146 GlyphHit,
1147 GlyphMiss,
1148 KernHit,
1149 KernMiss,
1150}
1151
1152struct SoftwareTextGlyphMetricsCache {
1153 glyphs: DirectMappedCache<GlyphMetricsKey, CachedGlyphMetrics>,
1154 kerns: DirectMappedCache<KernMetricsKey, f32>,
1155 #[cfg(test)]
1156 stats: SoftwareTextGlyphMetricsStats,
1157}
1158
1159impl SoftwareTextGlyphMetricsCache {
1160 fn new() -> Self {
1161 Self {
1162 glyphs: DirectMappedCache::with_slots_log2(SOFTWARE_TEXT_GLYPH_METRICS_SLOTS_LOG2),
1163 kerns: DirectMappedCache::with_slots_log2(SOFTWARE_TEXT_KERN_METRICS_SLOTS_LOG2),
1164 #[cfg(test)]
1165 stats: SoftwareTextGlyphMetricsStats::default(),
1166 }
1167 }
1168
1169 #[cfg(test)]
1170 fn stats(&self) -> SoftwareTextGlyphMetricsStats {
1171 self.stats
1172 }
1173
1174 #[inline(always)]
1177 fn count(&mut self, lookup: MetricsLookup) {
1178 #[cfg(test)]
1179 {
1180 let stats = &mut self.stats;
1181 let count = match lookup {
1182 MetricsLookup::GlyphHit => &mut stats.glyph_hits,
1183 MetricsLookup::GlyphMiss => &mut stats.glyph_misses,
1184 MetricsLookup::KernHit => &mut stats.kern_hits,
1185 MetricsLookup::KernMiss => &mut stats.kern_misses,
1186 };
1187 *count = count.saturating_add(1);
1188 }
1189 #[cfg(not(test))]
1190 let _ = lookup;
1191 }
1192
1193 #[inline]
1194 fn glyph_metrics<F, S>(
1195 &mut self,
1196 font: &SoftwareTextFont,
1197 scaled_font: &S,
1198 ch: char,
1199 ) -> CachedGlyphMetrics
1200 where
1201 F: Font,
1202 S: ScaleFont<F>,
1203 {
1204 if let Some((glyph_id, advance)) = font.font.ascii_glyph(ch) {
1207 self.count(MetricsLookup::GlyphHit);
1208 return CachedGlyphMetrics {
1209 glyph_id,
1210 advance_unscaled: advance.max(0.0),
1211 };
1212 }
1213 self.cached_glyph_metrics(font, scaled_font, ch)
1214 }
1215
1216 #[inline(never)]
1219 fn cached_glyph_metrics<F, S>(
1220 &mut self,
1221 font: &SoftwareTextFont,
1222 scaled_font: &S,
1223 ch: char,
1224 ) -> CachedGlyphMetrics
1225 where
1226 F: Font,
1227 S: ScaleFont<F>,
1228 {
1229 let key = GlyphMetricsKey {
1230 font_hash: font.glyph_map_hash,
1231 ch,
1232 };
1233 if let Some(metrics) = self.glyphs.get(&key) {
1234 self.count(MetricsLookup::GlyphHit);
1235 return metrics;
1236 }
1237
1238 let glyph_id = scaled_font.font().glyph_id(ch);
1239 let metrics = CachedGlyphMetrics {
1240 glyph_id,
1241 advance_unscaled: scaled_font.font().h_advance_unscaled(glyph_id).max(0.0),
1242 };
1243 self.glyphs.insert(key, metrics);
1244 self.count(MetricsLookup::GlyphMiss);
1245 metrics
1246 }
1247
1248 fn kern<F, S>(
1249 &mut self,
1250 font: &SoftwareTextFont,
1251 scaled_font: &S,
1252 previous: (char, GlyphId),
1253 next: (char, GlyphId),
1254 ) -> f32
1255 where
1256 F: Font,
1257 S: ScaleFont<F>,
1258 {
1259 if let Some(kern) = font.font.ascii_kern(previous, next) {
1260 self.count(MetricsLookup::KernHit);
1261 return kern;
1262 }
1263 let ((_, previous_id), (_, glyph_id)) = (previous, next);
1264 let key = KernMetricsKey {
1265 font_hash: font.content_hash(),
1266 previous_id: previous_id.0.into(),
1267 glyph_id: glyph_id.0.into(),
1268 };
1269 if let Some(kern) = self.kerns.get(&key) {
1270 self.count(MetricsLookup::KernHit);
1271 return kern;
1272 }
1273
1274 let kern = scaled_font.font().kern_unscaled(previous_id, glyph_id);
1275 self.kerns.insert(key, kern);
1276 self.count(MetricsLookup::KernMiss);
1277 kern
1278 }
1279}
1280
1281pub struct SoftwareTextMeasurer {
1283 fonts: SoftwareTextFontSet,
1284 cache: Mutex<SoftwareTextMetricsCache>,
1285 hyphenation: HyphenationDictionaryStore,
1286}
1287
1288impl SoftwareTextMeasurer {
1289 pub fn new(font: SoftwareTextFont, cache_capacity: usize) -> Self {
1292 Self::from_font_set(SoftwareTextFontSet::from_font(font), cache_capacity)
1293 }
1294
1295 pub fn from_font_set(fonts: SoftwareTextFontSet, cache_capacity: usize) -> Self {
1297 Self {
1298 fonts,
1299 cache: Mutex::new(SoftwareTextMetricsCache::new(cache_capacity)),
1300 hyphenation: HyphenationDictionaryStore::new(),
1301 }
1302 }
1303
1304 pub fn from_fonts_or_default(fonts: &[&'static [u8]], cache_capacity: usize) -> Self {
1307 Self::from_font_set(
1308 software_text_font_set_from_fonts_or_default(fonts),
1309 cache_capacity,
1310 )
1311 }
1312
1313 fn lock_cache(&self) -> MutexGuard<'_, SoftwareTextMetricsCache> {
1314 self.cache.lock().unwrap_or_else(PoisonError::into_inner)
1315 }
1316
1317 #[cfg(feature = "text-hyphenation")]
1319 pub fn register_hyphenation_dictionary_path(
1320 &self,
1321 locale: &str,
1322 path: impl AsRef<std::path::Path>,
1323 ) -> Result<(), HyphenationDictionaryError> {
1324 self.hyphenation.register_dictionary_path(locale, path)
1325 }
1326
1327 #[cfg(feature = "text-hyphenation")]
1329 pub fn register_hyphenation_dictionary_reader(
1330 &self,
1331 locale: &str,
1332 reader: &mut impl std::io::Read,
1333 ) -> Result<(), HyphenationDictionaryError> {
1334 self.hyphenation.register_dictionary_reader(locale, reader)
1335 }
1336}
1337
1338impl TextMeasurer for SoftwareTextMeasurer {
1339 fn measure(&self, text: &cranpose_ui::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
1340 self.lock_cache().get_or_measure(&self.fonts, text, style)
1341 }
1342
1343 fn begin_layout_pass(&self) {
1344 self.lock_cache().begin_layout_pass();
1345 }
1346
1347 fn measure_subsequence(
1348 &self,
1349 text: &cranpose_ui::text::AnnotatedString,
1350 range: std::ops::Range<usize>,
1351 style: &TextStyle,
1352 ) -> TextMetrics {
1353 let start = range.start.min(text.text.len());
1354 let end = range.end.max(start).min(text.text.len());
1355 self.lock_cache()
1356 .get_or_measure_range(&self.fonts, text, start..end, style)
1357 }
1358
1359 fn measure_line_prefix_widths(
1360 &self,
1361 text: &cranpose_ui::text::AnnotatedString,
1362 line_range: std::ops::Range<usize>,
1363 style: &TextStyle,
1364 ) -> Option<TextLinePrefixWidths> {
1365 if !measures_line_prefixes(text, &line_range, style) {
1366 return None;
1367 }
1368 self.lock_cache()
1369 .get_or_measure_prefix_widths(&self.fonts, text, line_range, style)
1370 }
1371
1372 fn measure_line_width(
1373 &self,
1374 text: &cranpose_ui::text::AnnotatedString,
1375 line_range: std::ops::Range<usize>,
1376 style: &TextStyle,
1377 ) -> Option<f32> {
1378 if requires_font_layout(&text.text, style) && is_single_line_range(text, &line_range) {
1379 return Some(self.measure_subsequence(text, line_range, style).width);
1380 }
1381 if !measures_line_prefixes(text, &line_range, style) {
1382 return None;
1383 }
1384 self.lock_cache()
1385 .get_or_measure_line_width(&self.fonts, text, line_range, style)
1386 }
1387
1388 fn line_height(&self, text: &cranpose_ui::text::AnnotatedString, style: &TextStyle) -> f32 {
1389 let font_size = resolve_font_size(style);
1390 max_line_height_for_annotated_text_with_resolver(text, style, font_size, &self.fonts)
1391 }
1392
1393 fn glyph_line_box(&self, style: &TextStyle) -> Option<(f32, f32)> {
1394 Some(font_glyph_line_box(style, self.fonts.resolve(style)?))
1395 }
1396
1397 fn first_baseline(&self, style: &TextStyle) -> Option<f32> {
1398 Some(self.line_box(style)?.baseline)
1399 }
1400
1401 fn line_box(&self, style: &TextStyle) -> Option<cranpose_ui::text::LineBox> {
1402 let font = self.fonts.resolve(style)?;
1403 Some(font_line_box(style, font, resolve_font_size(style)))
1404 }
1405
1406 fn line_box_and_height(
1407 &self,
1408 _node_id: Option<cranpose_core::NodeId>,
1409 text: &AnnotatedString,
1410 style: &TextStyle,
1411 ) -> (Option<cranpose_ui::text::LineBox>, f32) {
1412 let Some((font, covers)) = self.fonts.resolve_covering(&text.text, style) else {
1413 return (None, self.line_height(text, style));
1414 };
1415 let line_box = font_line_box(style, font, resolve_font_size(style));
1416 let height = if covers && text.span_styles.is_empty() {
1417 line_box.height
1418 } else {
1419 self.line_height(text, style)
1420 };
1421 (Some(line_box), height)
1422 }
1423
1424 fn visit_line_boxes(
1425 &self,
1426 text: &AnnotatedString,
1427 style: &TextStyle,
1428 visit: &mut dyn FnMut(cranpose_ui::text::LineBox),
1429 ) -> Option<()> {
1430 visit_annotated_line_boxes(text, style, &self.fonts, visit)
1431 }
1432
1433 fn get_offset_for_position(
1434 &self,
1435 text: &cranpose_ui::text::AnnotatedString,
1436 style: &TextStyle,
1437 x: f32,
1438 y: f32,
1439 ) -> usize {
1440 annotated_offset_for_position(text, style, x, y, &self.fonts)
1441 }
1442
1443 fn get_cursor_x_for_offset(
1444 &self,
1445 text: &cranpose_ui::text::AnnotatedString,
1446 style: &TextStyle,
1447 offset: usize,
1448 ) -> f32 {
1449 annotated_cursor_x_for_offset(text, style, offset, &self.fonts)
1450 }
1451
1452 fn layout(
1453 &self,
1454 text: &cranpose_ui::text::AnnotatedString,
1455 style: &TextStyle,
1456 ) -> TextLayoutResult {
1457 layout_annotated_text_with_font_set(text, style, &self.fonts)
1458 }
1459
1460 fn choose_auto_hyphen_break(
1461 &self,
1462 line: &str,
1463 style: &TextStyle,
1464 segment_start_char: usize,
1465 measured_break_char: usize,
1466 ) -> Option<usize> {
1467 self.hyphenation.choose_auto_hyphen_break(
1468 line,
1469 style,
1470 segment_start_char,
1471 measured_break_char,
1472 )
1473 }
1474}
1475
1476#[derive(Clone, Copy)]
1477enum GlyphRasterStyle {
1478 Fill,
1479 Stroke { width_px: f32 },
1480}
1481
1482#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1485pub struct SoftwareGlyphAtlasKey {
1486 pub font_hash: u64,
1488 pub glyph_id: u32,
1490 pub scale_x_bits: u32,
1492 pub scale_y_bits: u32,
1494 pub embolden_px_bits: u32,
1496 pub slant_bits: u32,
1499}
1500
1501#[derive(Clone)]
1503pub struct SoftwareGlyphAtlasMask {
1504 pub alpha: Arc<[u8]>,
1507 pub width: usize,
1509 pub height: usize,
1511}
1512
1513#[derive(Clone)]
1515pub struct SoftwareGlyphAtlasGlyph {
1516 pub key: SoftwareGlyphAtlasKey,
1518 pub mask: SoftwareGlyphAtlasMask,
1520 pub x: i32,
1522 pub y: i32,
1524 pub color: Color,
1526}
1527
1528#[derive(Clone, Copy)]
1530pub struct SoftwareGlyphAtlasPlacement {
1531 pub key: SoftwareGlyphAtlasKey,
1533 pub x: i32,
1535 pub y: i32,
1537 pub width: usize,
1539 pub height: usize,
1541 pub color: Color,
1543}
1544
1545#[derive(Clone)]
1547pub enum SoftwareGlyphAtlasRunGlyph {
1548 Cached(SoftwareGlyphAtlasPlacement),
1550 New(SoftwareGlyphAtlasGlyph),
1552}
1553
1554impl SoftwareGlyphAtlasRunGlyph {
1555 pub fn placement(&self) -> SoftwareGlyphAtlasPlacement {
1557 match self {
1558 Self::Cached(placement) => *placement,
1559 Self::New(glyph) => SoftwareGlyphAtlasPlacement {
1560 key: glyph.key,
1561 x: glyph.x,
1562 y: glyph.y,
1563 width: glyph.mask.width,
1564 height: glyph.mask.height,
1565 color: glyph.color,
1566 },
1567 }
1568 }
1569}
1570
1571#[derive(Clone)]
1573struct GlyphMask {
1574 alpha: Arc<[u8]>,
1575 width: usize,
1576 height: usize,
1577 origin_x: i32,
1578 origin_y: i32,
1579}
1580
1581struct RasterMask {
1584 alpha: Vec<f32>,
1585 width: usize,
1586 height: usize,
1587 origin_x: i32,
1588 origin_y: i32,
1589}
1590
1591impl RasterMask {
1592 fn into_glyph_mask(self) -> GlyphMask {
1593 GlyphMask {
1594 alpha: self
1595 .alpha
1596 .iter()
1597 .map(|&coverage| crate::text_mask_gamma::unit_to_u8(coverage))
1598 .collect(),
1599 width: self.width,
1600 height: self.height,
1601 origin_x: self.origin_x,
1602 origin_y: self.origin_y,
1603 }
1604 }
1605}
1606
1607#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1609pub struct SoftwareGlyphRasterCacheStats {
1610 pub entries: usize,
1612 pub hits: u64,
1614 pub misses: u64,
1616}
1617
1618struct SegmentGlyphMetrics {
1623 slots: [u8; ASCII_COUNT],
1625 metrics: Vec<CachedAtlasGlyphMetrics>,
1626}
1627
1628impl SegmentGlyphMetrics {
1629 fn new() -> Self {
1630 Self {
1631 slots: [0; ASCII_COUNT],
1632 metrics: Vec::new(),
1633 }
1634 }
1635
1636 fn clear(&mut self) {
1637 self.slots.fill(0);
1638 self.metrics.clear();
1639 }
1640
1641 fn get(&self, ch: char) -> Option<CachedAtlasGlyphMetrics> {
1642 let index = usize::from(self.slots[ascii_slot(ch)?]).checked_sub(1)?;
1643 self.metrics.get(index).copied()
1644 }
1645
1646 fn insert(&mut self, ch: char, metrics: CachedAtlasGlyphMetrics) {
1647 let Some(slot) = ascii_slot(ch) else {
1648 return;
1649 };
1650 let Ok(index) = u8::try_from(self.metrics.len() + 1) else {
1651 return;
1652 };
1653 self.slots[slot] = index;
1654 self.metrics.push(metrics);
1655 }
1656}
1657
1658#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1659enum GlyphRasterStyleKey {
1660 Fill,
1661 Stroke { width_px_bits: u32 },
1662}
1663
1664impl GlyphRasterStyleKey {
1665 fn from_style(style: GlyphRasterStyle) -> Self {
1666 match style {
1667 GlyphRasterStyle::Fill => Self::Fill,
1668 GlyphRasterStyle::Stroke { width_px } => Self::Stroke {
1669 width_px_bits: width_px.to_bits(),
1670 },
1671 }
1672 }
1673}
1674
1675#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1676struct GlyphMaskCacheKey {
1677 font_hash: u64,
1678 glyph_id: u32,
1679 scale_x_bits: u32,
1680 scale_y_bits: u32,
1681 raster_style: GlyphRasterStyleKey,
1682 embolden_px_bits: u32,
1683 slant_bits: u32,
1684}
1685
1686#[derive(Clone)]
1687struct CachedGlyphMask {
1688 alpha: Arc<[u8]>,
1689 width: usize,
1690 height: usize,
1691 origin_offset_x: i32,
1692 origin_offset_y: i32,
1693}
1694
1695impl CachedGlyphMask {
1696 fn from_mask(mask: GlyphMask, glyph: &Glyph) -> Self {
1697 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1698 Self {
1699 alpha: mask.alpha,
1700 width: mask.width,
1701 height: mask.height,
1702 origin_offset_x: mask.origin_x - glyph_x,
1703 origin_offset_y: mask.origin_y - glyph_y,
1704 }
1705 }
1706
1707 fn instantiate(&self, glyph: &Glyph) -> GlyphMask {
1708 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1709 GlyphMask {
1710 alpha: Arc::clone(&self.alpha),
1711 width: self.width,
1712 height: self.height,
1713 origin_x: glyph_x + self.origin_offset_x,
1714 origin_y: glyph_y + self.origin_offset_y,
1715 }
1716 }
1717
1718 fn placement(&self, glyph: &Glyph) -> (i32, i32, usize, usize) {
1719 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1720 (
1721 glyph_x + self.origin_offset_x,
1722 glyph_y + self.origin_offset_y,
1723 self.width,
1724 self.height,
1725 )
1726 }
1727
1728 fn atlas_metrics(&self, key: SoftwareGlyphAtlasKey) -> CachedAtlasGlyphMetrics {
1729 CachedAtlasGlyphMetrics {
1730 key,
1731 width: self.width,
1732 height: self.height,
1733 origin_offset_x: self.origin_offset_x,
1734 origin_offset_y: self.origin_offset_y,
1735 }
1736 }
1737}
1738
1739#[derive(Clone, Copy)]
1740struct CachedAtlasGlyphMetrics {
1741 key: SoftwareGlyphAtlasKey,
1742 width: usize,
1743 height: usize,
1744 origin_offset_x: i32,
1745 origin_offset_y: i32,
1746}
1747
1748impl CachedAtlasGlyphMetrics {
1749 fn placement(self, glyph: &Glyph, color: Color) -> SoftwareGlyphAtlasPlacement {
1750 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1751 SoftwareGlyphAtlasPlacement {
1752 key: self.key,
1753 x: glyph_x + self.origin_offset_x,
1754 y: glyph_y + self.origin_offset_y,
1755 width: self.width,
1756 height: self.height,
1757 color,
1758 }
1759 }
1760}
1761
1762pub struct SoftwareGlyphRasterCache {
1764 masks: BoundedLruCache<GlyphMaskCacheKey, CachedGlyphMask>,
1765 kerns: DirectMappedCache<KernMetricsKey, f32>,
1766 segment_metrics: SegmentGlyphMetrics,
1767 hits: u64,
1768 misses: u64,
1769}
1770
1771impl SoftwareGlyphRasterCache {
1772 pub fn with_capacity_at_least_one(capacity: usize) -> Self {
1774 Self {
1775 masks: BoundedLruCache::with_capacity_at_least_one(capacity),
1776 kerns: DirectMappedCache::with_slots_log2(SOFTWARE_TEXT_KERN_METRICS_SLOTS_LOG2),
1777 segment_metrics: SegmentGlyphMetrics::new(),
1778 hits: 0,
1779 misses: 0,
1780 }
1781 }
1782
1783 fn kern_unscaled(
1787 &mut self,
1788 font_hash: u64,
1789 font: &impl Font,
1790 previous: GlyphId,
1791 glyph: GlyphId,
1792 ) -> f32 {
1793 let key = KernMetricsKey {
1794 font_hash,
1795 previous_id: previous.0.into(),
1796 glyph_id: glyph.0.into(),
1797 };
1798 if let Some(kern) = self.kerns.get(&key) {
1799 return kern;
1800 }
1801 let kern = font.kern_unscaled(previous, glyph);
1802 self.kerns.insert(key, kern);
1803 kern
1804 }
1805
1806 pub fn stats(&self) -> SoftwareGlyphRasterCacheStats {
1808 SoftwareGlyphRasterCacheStats {
1809 entries: self.masks.len(),
1810 hits: self.hits,
1811 misses: self.misses,
1812 }
1813 }
1814
1815 fn get(&mut self, key: &GlyphMaskCacheKey, glyph: &Glyph) -> Option<GlyphMask> {
1816 let mask = self.masks.get(key)?.instantiate(glyph);
1817 self.hits = self.hits.saturating_add(1);
1818 Some(mask)
1819 }
1820
1821 fn get_atlas_placement(
1822 &mut self,
1823 key: &GlyphMaskCacheKey,
1824 glyph: &Glyph,
1825 ) -> Option<(SoftwareGlyphAtlasKey, i32, i32, usize, usize)> {
1826 let atlas_key = glyph_atlas_key_from_mask_key(*key)?;
1827 let (x, y, width, height) = self.masks.get(key)?.placement(glyph);
1828 self.hits = self.hits.saturating_add(1);
1829 Some((atlas_key, x, y, width, height))
1830 }
1831
1832 fn get_atlas_metrics(&mut self, key: &GlyphMaskCacheKey) -> Option<CachedAtlasGlyphMetrics> {
1833 let atlas_key = glyph_atlas_key_from_mask_key(*key)?;
1834 let metrics = self.masks.get(key)?.atlas_metrics(atlas_key);
1835 self.hits = self.hits.saturating_add(1);
1836 Some(metrics)
1837 }
1838
1839 pub fn atlas_glyph_for_placement(
1842 &mut self,
1843 placement: &SoftwareGlyphAtlasPlacement,
1844 ) -> Option<SoftwareGlyphAtlasGlyph> {
1845 let key = GlyphMaskCacheKey {
1846 font_hash: placement.key.font_hash,
1847 glyph_id: placement.key.glyph_id,
1848 scale_x_bits: placement.key.scale_x_bits,
1849 scale_y_bits: placement.key.scale_y_bits,
1850 raster_style: GlyphRasterStyleKey::Fill,
1851 embolden_px_bits: placement.key.embolden_px_bits,
1852 slant_bits: placement.key.slant_bits,
1853 };
1854 let mask = self.masks.get(&key)?;
1855 self.hits = self.hits.saturating_add(1);
1856 Some(SoftwareGlyphAtlasGlyph {
1857 key: placement.key,
1858 mask: SoftwareGlyphAtlasMask {
1859 alpha: Arc::clone(&mask.alpha),
1860 width: mask.width,
1861 height: mask.height,
1862 },
1863 x: placement.x,
1864 y: placement.y,
1865 color: placement.color,
1866 })
1867 }
1868
1869 fn put(&mut self, key: GlyphMaskCacheKey, glyph: &Glyph, mask: GlyphMask) -> GlyphMask {
1870 let cached = CachedGlyphMask::from_mask(mask, glyph);
1871 let mask = cached.instantiate(glyph);
1872 self.masks.put(key, cached);
1873 self.misses = self.misses.saturating_add(1);
1874 mask
1875 }
1876}
1877
1878struct RasterFontRef<'a, F> {
1879 font: &'a F,
1880 ab_glyph_scale_factor: f32,
1881 weight: FontWeight,
1882 style: FontStyle,
1883 tracking: &'a FontTracking,
1884}
1885
1886#[derive(Clone, Copy)]
1887struct TextWeightSynthesis {
1888 embolden_px: f32,
1889 advance_scale: f32,
1890}
1891
1892impl TextWeightSynthesis {
1893 fn none() -> Self {
1894 Self {
1895 embolden_px: 0.0,
1896 advance_scale: 1.0,
1897 }
1898 }
1899
1900 const FAKE_BOLD_MIN_WEIGHT: u16 = 600;
1901 const FAKE_BOLD_MIN_DELTA: u16 = 200;
1902
1903 fn for_style(
1904 style: &TextStyle,
1905 resolved_weight: FontWeight,
1906 font_size: f32,
1907 scale: f32,
1908 ) -> Self {
1909 let requested_weight = style.span_style.font_weight.unwrap_or_default();
1910 if requested_weight <= resolved_weight {
1911 return Self::none();
1912 }
1913 if requested_weight.value() < Self::FAKE_BOLD_MIN_WEIGHT
1914 || requested_weight.value() - resolved_weight.value() < Self::FAKE_BOLD_MIN_DELTA
1915 {
1916 return Self::none();
1917 }
1918
1919 let synthesis = style
1920 .span_style
1921 .font_synthesis
1922 .unwrap_or(FontSynthesis::All);
1923 if !matches!(synthesis, FontSynthesis::All | FontSynthesis::Weight) {
1924 return Self::none();
1925 }
1926
1927 let weight_delta = (requested_weight.value() - resolved_weight.value()) as f32;
1928 let strength = (weight_delta / 300.0).clamp(0.0, 1.5);
1929 Self {
1930 embolden_px: (font_size * scale * 0.055 * strength).clamp(0.0, 3.0 * scale),
1931 advance_scale: 1.0 + 0.085 * strength.min(1.0),
1932 }
1933 }
1934
1935 fn apply_width(self, width: f32) -> f32 {
1936 width * self.advance_scale
1937 }
1938}
1939
1940#[derive(Clone, Copy)]
1941struct TextStyleSynthesis {
1942 slant: f32,
1943 font_size: f32,
1944 scale: f32,
1945}
1946
1947impl TextStyleSynthesis {
1948 fn none() -> Self {
1949 Self {
1950 slant: 0.0,
1951 font_size: 0.0,
1952 scale: 1.0,
1953 }
1954 }
1955
1956 fn for_style(style: &TextStyle, resolved_style: FontStyle, font_size: f32, scale: f32) -> Self {
1957 let requested_style = style.span_style.font_style.unwrap_or_default();
1958 if requested_style != FontStyle::Italic || resolved_style == FontStyle::Italic {
1959 return Self::none();
1960 }
1961
1962 let synthesis = style
1963 .span_style
1964 .font_synthesis
1965 .unwrap_or(FontSynthesis::All);
1966 if !matches!(synthesis, FontSynthesis::All | FontSynthesis::Style) {
1967 return Self::none();
1968 }
1969
1970 Self {
1971 slant: 0.22,
1972 font_size,
1973 scale,
1974 }
1975 }
1976
1977 fn visual_overhang_px(self) -> f32 {
1978 if self.slant <= 0.0 || !self.font_size.is_finite() || !self.scale.is_finite() {
1979 return 0.0;
1980 }
1981 whole::ceil(self.font_size * self.scale * self.slant).max(0.0)
1982 }
1983}
1984
1985pub struct TextPixels {
1989 pub width: u32,
1991 pub height: u32,
1993 pub pixels: Vec<u8>,
1995}
1996
1997impl TextPixels {
1998 pub fn into_image(self) -> Option<ImageBitmap> {
2000 ImageBitmap::from_rgba8(self.width, self.height, self.pixels).ok()
2001 }
2002}
2003
2004pub fn rasterize_text_to_image(
2008 text: &str,
2009 rect: Rect,
2010 style: &TextStyle,
2011 fallback_color: Color,
2012 font_size: f32,
2013 scale: f32,
2014 font: &SoftwareTextFont,
2015) -> Option<ImageBitmap> {
2016 rasterize_text_pixels(
2017 text,
2018 rect,
2019 style,
2020 fallback_color,
2021 font_size,
2022 scale,
2023 font,
2024 None,
2025 )
2026 .and_then(TextPixels::into_image)
2027}
2028
2029#[expect(clippy::too_many_arguments)]
2031pub fn rasterize_text_to_image_with_glyph_cache(
2032 text: &str,
2033 rect: Rect,
2034 style: &TextStyle,
2035 fallback_color: Color,
2036 font_size: f32,
2037 scale: f32,
2038 font: &SoftwareTextFont,
2039 glyph_cache: &mut SoftwareGlyphRasterCache,
2040) -> Option<ImageBitmap> {
2041 rasterize_text_pixels(
2042 text,
2043 rect,
2044 style,
2045 fallback_color,
2046 font_size,
2047 scale,
2048 font,
2049 Some(glyph_cache),
2050 )
2051 .and_then(TextPixels::into_image)
2052}
2053
2054#[expect(clippy::too_many_arguments)]
2055fn rasterize_text_pixels(
2056 text: &str,
2057 rect: Rect,
2058 style: &TextStyle,
2059 fallback_color: Color,
2060 font_size: f32,
2061 scale: f32,
2062 font: &SoftwareTextFont,
2063 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
2064) -> Option<TextPixels> {
2065 if requires_font_layout(text, style) {
2066 return rasterize_styled_text_region(
2067 StyledTextRef {
2068 text,
2069 span_styles: &[],
2070 },
2071 rect,
2072 Point::new(rect.x, rect.y),
2073 style,
2074 fallback_color,
2075 font_size,
2076 scale,
2077 font.into(),
2078 glyph_cache,
2079 );
2080 }
2081 let shaped = font.shaped_for(style);
2082 let font = &*shaped;
2083 rasterize_text_to_image_impl(
2084 TextRasterImageRequest {
2085 text,
2086 rect,
2087 style,
2088 fallback_color,
2089 font_size,
2090 scale,
2091 },
2092 font.raster_ref(),
2093 font.content_hash(),
2094 glyph_cache,
2095 )
2096}
2097
2098#[derive(Clone, Copy)]
2103pub struct StyledTextRef<'a> {
2104 pub text: &'a str,
2106 pub span_styles: &'a [RangeStyle<SpanStyle>],
2108}
2109
2110impl StyledTextRef<'_> {
2111 fn is_empty(&self) -> bool {
2112 self.text.is_empty()
2113 }
2114
2115 pub(crate) fn span_boundaries(&self) -> cranpose_ui::text::SpanBoundaries {
2116 let mut boundaries: cranpose_ui::text::SpanBoundaries =
2117 smallvec::smallvec![0, self.text.len()];
2118 for span in self.span_styles {
2119 boundaries.push(span.range.start);
2120 boundaries.push(span.range.end);
2121 }
2122 boundaries.sort_unstable();
2123 boundaries.dedup();
2124 boundaries.retain(|b| *b <= self.text.len() && self.text.is_char_boundary(*b));
2125 boundaries
2126 }
2127}
2128
2129impl<'a> From<&'a AnnotatedString> for StyledTextRef<'a> {
2130 fn from(text: &'a AnnotatedString) -> Self {
2131 Self {
2132 text: text.text.as_str(),
2133 span_styles: &text.span_styles,
2134 }
2135 }
2136}
2137
2138impl<'a> From<&'a RenderString> for StyledTextRef<'a> {
2139 fn from(text: &'a RenderString) -> Self {
2140 Self {
2141 text: text.text(),
2142 span_styles: text.span_styles(),
2143 }
2144 }
2145}
2146
2147fn annotated_line_alignment_offsets(
2148 layout: &AnnotatedTextLayout<'_>,
2149 text: &StyledTextRef<'_>,
2150 style: &TextStyle,
2151 scale: f32,
2152) -> Option<Vec<f32>> {
2153 if !text.text.contains('\n') {
2154 return None;
2155 }
2156 let align_fraction = cranpose_ui::text::text_align_fraction(style, text.text);
2157 if align_fraction == 0.0 {
2158 return None;
2159 }
2160 let mut offsets = Vec::with_capacity(layout.lines.len());
2161 let mut block = 0.0_f32;
2162 for index in 0..layout.lines.len() {
2163 let advance = layout.walk_line(index, 0.0, |segment| {
2164 Some(annotated_raster_segment_advance(&segment, scale))
2165 })?;
2166 block = block.max(advance);
2167 offsets.push(advance);
2168 }
2169 for offset in &mut offsets {
2170 *offset = ((block - *offset) * align_fraction).max(0.0);
2171 }
2172 Some(offsets)
2173}
2174
2175fn segment_advance_px(
2176 font: &impl Font,
2177 content: &str,
2178 font_px_size: f32,
2179 letter_spacing: f32,
2180) -> f32 {
2181 let scaled_font = font.as_scaled(PxScale::from(font_px_size));
2182 let mut caret = 0.0f32;
2183 let mut previous = None;
2184 for ch in content.chars() {
2185 let glyph_id = scaled_font.glyph_id(ch);
2186 if let Some(previous_id) = previous {
2187 caret += scaled_font.kern(previous_id, glyph_id);
2188 }
2189 caret += letter_spacing + scaled_font.h_advance(glyph_id);
2190 previous = Some(glyph_id);
2191 }
2192 caret.max(0.0)
2193}
2194
2195fn annotated_raster_segment_advance(segment: &AnnotatedTextSegment<'_>, scale: f32) -> f32 {
2196 #[cfg(feature = "text-shaping")]
2197 if requires_font_layout(segment.text, segment.style)
2198 && let Some(run) = segment.font.shaped_run(segment.text, segment.style)
2199 {
2200 return shaped_run_width(&run, segment.font, segment.style, segment.font_size, scale);
2201 }
2202 let font = segment.font.shaped_for(segment.style);
2203 segment_advance_px(
2204 &font.font,
2205 segment.text,
2206 font.ab_glyph_px_size(segment.font_size) * scale,
2207 font.metadata
2208 .tracking
2209 .resolve(segment.style, segment.font_size)
2210 * scale,
2211 )
2212}
2213
2214fn text_render_request_is_degenerate(
2215 text_is_empty: bool,
2216 rect: Rect,
2217 font_size: f32,
2218 scale: f32,
2219) -> bool {
2220 text_is_empty
2221 || rect.width <= 0.0
2222 || rect.height <= 0.0
2223 || !font_size.is_finite()
2224 || font_size <= 0.0
2225 || !scale.is_finite()
2226 || scale <= 0.0
2227}
2228
2229#[derive(Clone, Copy)]
2230struct TextSegmentMetrics {
2231 font_px_size: f32,
2232 letter_spacing: f32,
2233 align_fraction: f32,
2234 weight_synthesis: TextWeightSynthesis,
2235 style_synthesis: TextStyleSynthesis,
2236 line_height: f32,
2237 first_baseline_y: f32,
2238}
2239
2240fn text_segment_metrics(
2241 text: &str,
2242 local_rect: Rect,
2243 style: &TextStyle,
2244 font_size: f32,
2245 scale: f32,
2246 font: &SoftwareTextFont,
2247) -> TextSegmentMetrics {
2248 let font_px_size = font.ab_glyph_px_size(font_size) * scale;
2249 let letter_spacing = font.metadata.tracking.resolve(style, font_size) * scale;
2250 let align_fraction = cranpose_ui::text::text_align_fraction(style, text);
2251 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, scale);
2252 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, scale);
2253 let metrics = vertical_metrics(&font.font, font_px_size);
2254 let line_box = line_box_for(style, metrics, asked_line_height(style, scale), 1.0);
2255 TextSegmentMetrics {
2256 font_px_size,
2257 letter_spacing,
2258 align_fraction,
2259 weight_synthesis,
2260 style_synthesis,
2261 line_height: line_box.height,
2262 first_baseline_y: local_rect.y + line_box.first_baseline(),
2263 }
2264}
2265
2266fn text_segment_supports_solid_atlas(style: &TextStyle) -> bool {
2267 style_can_atlas_solid_fill(style)
2268 && style
2269 .paragraph_style
2270 .text_motion
2271 .unwrap_or(TextMotion::Static)
2272 == TextMotion::Static
2273}
2274
2275fn style_can_rasterize_direct_solid(style: &TextStyle) -> bool {
2276 !style
2277 .span_style
2278 .shadow
2279 .is_some_and(|shadow| shadow.color.3 > 0.0)
2280 && matches!(
2281 style.span_style.brush.as_ref(),
2282 None | Some(Brush::Solid(_))
2283 )
2284}
2285
2286fn style_can_atlas_solid_fill(style: &TextStyle) -> bool {
2287 style_can_rasterize_direct_solid(style)
2288 && match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
2289 TextDrawStyle::Fill => true,
2290 TextDrawStyle::Stroke { width } => !width.is_finite() || width <= 0.0,
2291 }
2292}
2293
2294fn visit_annotated_text_segments<'a>(
2295 text: impl Into<StyledTextRef<'a>>,
2296 style: &TextStyle,
2297 font_size: f32,
2298 scale: f32,
2299 fonts: &'a SoftwareTextFontSet,
2300 visit: impl FnMut(AnnotatedTextSegment<'_>) -> Option<f32>,
2301) -> Option<TextMetrics> {
2302 let text = text.into();
2303 let layout = AnnotatedTextLayout::new(text, style, font_size, scale, 1.0, fonts)?;
2304 let offsets = annotated_line_alignment_offsets(&layout, &text, style, scale);
2305 layout.walk(offsets.as_deref(), visit)
2306}
2307
2308#[expect(clippy::too_many_arguments)]
2311pub fn rasterize_annotated_text_to_image_with_glyph_cache<'a>(
2312 text: impl Into<StyledTextRef<'a>>,
2313 rect: Rect,
2314 style: &TextStyle,
2315 fallback_color: Color,
2316 font_size: f32,
2317 scale: f32,
2318 fonts: &SoftwareTextFontSet,
2319 glyph_cache: &mut SoftwareGlyphRasterCache,
2320) -> Option<ImageBitmap> {
2321 let text = text.into();
2322 rasterize_annotated_text_region(
2323 text,
2324 rect,
2325 Point::new(rect.x, rect.y),
2326 style,
2327 fallback_color,
2328 font_size,
2329 scale,
2330 fonts,
2331 Some(glyph_cache),
2332 )
2333}
2334
2335#[expect(clippy::too_many_arguments)]
2338pub fn rasterize_annotated_text_region_pixels<'a>(
2339 text: impl Into<StyledTextRef<'a>>,
2340 rect: Rect,
2341 text_origin: Point,
2342 style: &TextStyle,
2343 fallback_color: Color,
2344 font_size: f32,
2345 scale: f32,
2346 fonts: &SoftwareTextFontSet,
2347 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
2348) -> Option<TextPixels> {
2349 rasterize_styled_text_region(
2350 text.into(),
2351 rect,
2352 text_origin,
2353 style,
2354 fallback_color,
2355 font_size,
2356 scale,
2357 fonts.into(),
2358 glyph_cache,
2359 )
2360}
2361
2362#[expect(clippy::too_many_arguments)]
2366pub fn rasterize_annotated_text_region<'a>(
2367 text: impl Into<StyledTextRef<'a>>,
2368 rect: Rect,
2369 text_origin: Point,
2370 style: &TextStyle,
2371 fallback_color: Color,
2372 font_size: f32,
2373 scale: f32,
2374 fonts: &SoftwareTextFontSet,
2375 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
2376) -> Option<ImageBitmap> {
2377 rasterize_annotated_text_region_pixels(
2378 text,
2379 rect,
2380 text_origin,
2381 style,
2382 fallback_color,
2383 font_size,
2384 scale,
2385 fonts,
2386 glyph_cache,
2387 )
2388 .and_then(TextPixels::into_image)
2389}
2390
2391#[expect(clippy::too_many_arguments)]
2392#[inline(never)]
2393fn rasterize_styled_text_region(
2394 text: StyledTextRef<'_>,
2395 rect: Rect,
2396 text_origin: Point,
2397 style: &TextStyle,
2398 fallback_color: Color,
2399 font_size: f32,
2400 scale: f32,
2401 fonts: FontResolver<'_>,
2402 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
2403) -> Option<TextPixels> {
2404 if text_render_request_is_degenerate(text.is_empty(), rect, font_size, scale) {
2405 return None;
2406 }
2407 if text.span_styles.is_empty()
2408 && !requires_font_layout(text.text, style)
2409 && text_origin.x == rect.x
2410 && text_origin.y == rect.y
2411 && let Some(font) = fonts.single_font_for_text(text.text, style)
2412 {
2413 return rasterize_text_pixels(
2414 text.text,
2415 rect,
2416 style,
2417 fallback_color,
2418 font_size,
2419 scale,
2420 font,
2421 glyph_cache.as_deref_mut(),
2422 );
2423 }
2424 let layout = AnnotatedTextLayout::new(text, style, font_size, scale, 1.0, fonts)?;
2425 let solid = layout.all_styles(style_can_rasterize_direct_solid);
2426 let width = whole::ceil(rect.width).max(1.0) as u32;
2427 let height = whole::ceil(rect.height).max(1.0) as u32;
2428 let mut canvas = TextRasterCanvas::new(width, height, solid);
2429 let offsets = annotated_line_alignment_offsets(&layout, &text, style, scale);
2430 let brush_extents = layout.has_non_solid_brush().then(|| {
2431 layout.brush_extents(offsets.as_deref(), scale, |segment| {
2432 annotated_raster_segment_advance(segment, scale)
2433 })
2434 });
2435 layout.walk(offsets.as_deref(), |segment| {
2436 let static_text_motion = segment
2437 .style
2438 .paragraph_style
2439 .text_motion
2440 .unwrap_or(TextMotion::Static)
2441 == TextMotion::Static;
2442 let origin = Point::new(
2443 text_origin.x - rect.x + segment.origin.x,
2444 text_origin.y - rect.y + segment.origin.y,
2445 );
2446 let local_rect = Rect {
2447 x: if static_text_motion {
2448 whole::round(origin.x)
2449 } else {
2450 origin.x + rect.x.fract()
2451 },
2452 y: if static_text_motion {
2453 origin.y
2454 } else {
2455 origin.y + rect.y.fract()
2456 },
2457 width: width as f32,
2458 height: height as f32,
2459 };
2460 let fallback = Brush::solid(segment.style.resolve_text_color(fallback_color));
2461 let (brush, alpha) =
2462 segment
2463 .style
2464 .span_style
2465 .brush
2466 .as_ref()
2467 .map_or((&fallback, 1.0), |brush| {
2468 (
2469 brush,
2470 segment
2471 .style
2472 .span_style
2473 .alpha
2474 .unwrap_or(1.0)
2475 .clamp(0.0, 1.0),
2476 )
2477 });
2478 let brush_rect = annotated_brush_rect(
2479 brush,
2480 brush_extents.as_deref().unwrap_or_default(),
2481 &segment,
2482 text_origin,
2483 rect,
2484 )?;
2485 let paint = GlyphPaint {
2486 brush,
2487 alpha,
2488 shadow: segment
2489 .style
2490 .span_style
2491 .shadow
2492 .filter(|shadow| shadow.color.3 > 0.0),
2493 brush_rect,
2494 canvas_origin: Point::new(rect.x, rect.y),
2495 scale,
2496 static_text_motion,
2497 };
2498 Some(visit_text_segment_masks(
2499 segment.text,
2500 local_rect,
2501 segment.style,
2502 segment.font_size,
2503 scale,
2504 segment.font,
2505 glyph_cache.as_deref_mut(),
2506 &GlyphRasterClip::new(width, height, &paint),
2507 |mask| canvas.paint(mask, &paint),
2508 ))
2509 })?;
2510 canvas.into_pixels()
2511}
2512
2513fn annotated_brush_rect(
2514 brush: &Brush,
2515 extents: &[AnnotatedBrushExtent],
2516 segment: &AnnotatedTextSegment<'_>,
2517 text_origin: Point,
2518 fallback: Rect,
2519) -> Option<Rect> {
2520 if matches!(brush, Brush::Solid(_)) {
2521 return Some(fallback);
2522 }
2523 let index = extents
2524 .binary_search_by_key(&(segment.line_index, segment.style_index), |extent| {
2525 (extent.line_index, extent.style_index)
2526 })
2527 .ok()?;
2528 Some(extents[index].rect.translate(text_origin.x, text_origin.y))
2529}
2530
2531#[expect(clippy::too_many_arguments)]
2532fn walk_solid_text_atlas_segments<'a, T>(
2533 text: impl Into<StyledTextRef<'a>>,
2534 rect: Rect,
2535 style: &TextStyle,
2536 fallback_color: Color,
2537 font_size: f32,
2538 scale: f32,
2539 fonts: &SoftwareTextFontSet,
2540 glyph_cache: &mut SoftwareGlyphRasterCache,
2541 out: &mut Vec<T>,
2542 mut collect_segment: impl FnMut(
2543 &str,
2544 Rect,
2545 &TextStyle,
2546 Color,
2547 f32,
2548 f32,
2549 &SoftwareTextFont,
2550 &mut SoftwareGlyphRasterCache,
2551 &mut Vec<T>,
2552 ) -> Option<f32>,
2553) -> Option<()> {
2554 let text = text.into();
2555 if text_render_request_is_degenerate(text.is_empty(), rect, font_size, scale) {
2556 return Some(());
2557 }
2558 if text.span_styles.is_empty() && !requires_font_layout(text.text, style) {
2559 if !text_segment_supports_solid_atlas(style) {
2560 return None;
2561 }
2562 if let Some(font) = fonts.single_font_for_text(text.text, style) {
2563 return collect_segment(
2564 text.text,
2565 Rect {
2566 x: 0.0,
2567 y: 0.0,
2568 width: rect.width,
2569 height: rect.height,
2570 },
2571 style,
2572 style.resolve_text_color(fallback_color),
2573 font_size,
2574 scale,
2575 font,
2576 glyph_cache,
2577 out,
2578 )
2579 .map(|_| ());
2580 }
2581 }
2582 let initial_len = out.len();
2583 let result = visit_annotated_text_segments(text, style, font_size, scale, fonts, |segment| {
2584 if !text_segment_supports_solid_atlas(segment.style) {
2585 return None;
2586 }
2587 collect_segment(
2588 segment.text,
2589 Rect {
2590 x: whole::round(segment.origin.x),
2591 y: segment.origin.y,
2592 width: rect.width,
2593 height: rect.height,
2594 },
2595 segment.style,
2596 segment.style.resolve_text_color(fallback_color),
2597 segment.font_size,
2598 scale,
2599 segment.font,
2600 glyph_cache,
2601 out,
2602 )
2603 });
2604 if result.is_none() {
2605 out.truncate(initial_len);
2606 }
2607 result.map(|_| ())
2608}
2609
2610#[expect(clippy::too_many_arguments)]
2614pub fn collect_solid_text_atlas_glyphs(
2615 text: &AnnotatedString,
2616 rect: Rect,
2617 style: &TextStyle,
2618 fallback_color: Color,
2619 font_size: f32,
2620 scale: f32,
2621 fonts: &SoftwareTextFontSet,
2622 glyph_cache: &mut SoftwareGlyphRasterCache,
2623 out: &mut Vec<SoftwareGlyphAtlasGlyph>,
2624) -> Option<()> {
2625 walk_solid_text_atlas_segments(
2626 text,
2627 rect,
2628 style,
2629 fallback_color,
2630 font_size,
2631 scale,
2632 fonts,
2633 glyph_cache,
2634 out,
2635 collect_text_segment_solid_atlas_glyphs,
2636 )
2637}
2638
2639#[expect(clippy::too_many_arguments)]
2643pub fn collect_cached_solid_text_atlas_placements(
2644 text: &AnnotatedString,
2645 rect: Rect,
2646 style: &TextStyle,
2647 fallback_color: Color,
2648 font_size: f32,
2649 scale: f32,
2650 fonts: &SoftwareTextFontSet,
2651 glyph_cache: &mut SoftwareGlyphRasterCache,
2652 out: &mut Vec<SoftwareGlyphAtlasPlacement>,
2653) -> Option<()> {
2654 walk_solid_text_atlas_segments(
2655 text,
2656 rect,
2657 style,
2658 fallback_color,
2659 font_size,
2660 scale,
2661 fonts,
2662 glyph_cache,
2663 out,
2664 collect_text_segment_cached_solid_atlas_placements,
2665 )
2666}
2667
2668#[expect(clippy::too_many_arguments)]
2671pub fn collect_solid_text_atlas_run<'a>(
2672 text: impl Into<StyledTextRef<'a>>,
2673 rect: Rect,
2674 style: &TextStyle,
2675 fallback_color: Color,
2676 font_size: f32,
2677 scale: f32,
2678 fonts: &SoftwareTextFontSet,
2679 glyph_cache: &mut SoftwareGlyphRasterCache,
2680 out: &mut Vec<SoftwareGlyphAtlasRunGlyph>,
2681) -> Option<()> {
2682 walk_solid_text_atlas_segments(
2683 text,
2684 rect,
2685 style,
2686 fallback_color,
2687 font_size,
2688 scale,
2689 fonts,
2690 glyph_cache,
2691 out,
2692 collect_text_segment_solid_atlas_run,
2693 )
2694}
2695
2696pub fn measure_text_with_font(
2698 text: &str,
2699 style: &TextStyle,
2700 font_size: f32,
2701 font: &SoftwareTextFont,
2702) -> TextMetrics {
2703 #[cfg(feature = "text-shaping")]
2704 if requires_font_layout(text, style) {
2705 return measure_annotated_text_with_resolver(
2706 StyledTextRef {
2707 text,
2708 span_styles: &[],
2709 },
2710 style,
2711 font_size,
2712 font,
2713 None,
2714 );
2715 }
2716 measure_text_impl(text, style, font_size, font.shaped_for(style).raster_ref())
2717}
2718
2719fn measure_text_with_font_cached(
2720 text: &str,
2721 style: &TextStyle,
2722 font_size: f32,
2723 font: &SoftwareTextFont,
2724 cache: &mut SoftwareTextMetricsCache,
2725) -> TextMetrics {
2726 #[cfg(feature = "text-shaping")]
2727 if requires_font_layout(text, style) {
2728 return measure_shaped_text(text, style, font_size, font);
2729 }
2730 measure_text_impl_cached(text, style, font_size, &font.shaped_for(style), cache)
2731}
2732
2733pub fn measure_annotated_text_with_font(
2736 text: &AnnotatedString,
2737 style: &TextStyle,
2738 font_size: f32,
2739 font: &SoftwareTextFont,
2740) -> TextMetrics {
2741 if text.span_styles.is_empty() && !requires_font_layout(&text.text, style) {
2742 return measure_text_with_font(text.text.as_str(), style, font_size, font);
2743 }
2744 measure_annotated_text_with_resolver(text, style, font_size, font, None)
2745}
2746
2747pub fn measure_annotated_text_with_font_set(
2750 text: &AnnotatedString,
2751 style: &TextStyle,
2752 font_size: f32,
2753 fonts: &SoftwareTextFontSet,
2754) -> TextMetrics {
2755 if text.span_styles.is_empty()
2756 && let Some(font) = fonts.single_font_for_text(text.text.as_str(), style)
2757 {
2758 return measure_text_with_font(text.text.as_str(), style, font_size, font);
2759 }
2760 measure_annotated_text_with_resolver(text, style, font_size, fonts, None)
2761}
2762
2763fn measure_annotated_text_with_font_set_cached(
2764 text: &AnnotatedString,
2765 style: &TextStyle,
2766 font_size: f32,
2767 fonts: &SoftwareTextFontSet,
2768 cache: &mut SoftwareTextMetricsCache,
2769) -> TextMetrics {
2770 if text.span_styles.is_empty()
2771 && let Some(font) = fonts.single_font_for_text(text.text.as_str(), style)
2772 {
2773 return measure_text_with_font_cached(text.text.as_str(), style, font_size, font, cache);
2774 }
2775 measure_annotated_text_with_resolver(text, style, font_size, fonts, Some(cache))
2776}
2777
2778pub fn text_offset_for_position_with_font(
2781 text: &str,
2782 style: &TextStyle,
2783 x: f32,
2784 y: f32,
2785 font: &SoftwareTextFont,
2786) -> usize {
2787 if requires_font_layout(text, style) {
2788 return annotated_offset_for_position(
2789 StyledTextRef {
2790 text,
2791 span_styles: &[],
2792 },
2793 style,
2794 x,
2795 y,
2796 font,
2797 );
2798 }
2799 if text.is_empty() {
2800 return 0;
2801 }
2802
2803 let shaped = font.shaped_for(style);
2804 let font = &*shaped;
2805 let font_size = resolve_font_size(style);
2806 let line_box = font_line_box(style, font, font_size);
2807 let line_height = line_box.height;
2808
2809 let line_index = whole::floor((y + line_box.trim_top) / line_height).max(0.0) as usize;
2810 let lines: Vec<&str> = text.split('\n').collect();
2811 let target_line = line_index.min(lines.len().saturating_sub(1));
2812
2813 let mut line_start_byte = 0;
2814 for line in lines.iter().take(target_line) {
2815 line_start_byte += line.len() + 1;
2816 }
2817
2818 let line_text = lines.get(target_line).unwrap_or(&"");
2819 if line_text.is_empty() {
2820 return line_start_byte;
2821 }
2822
2823 let mut best_offset = 0;
2824 let mut best_distance = f32::INFINITY;
2825 let mut current_byte_offset = 0;
2826
2827 for c in line_text.chars() {
2828 let prefix = &line_text[..current_byte_offset];
2829 let glyph_x = measure_text_impl(prefix, style, font_size, font.raster_ref()).width;
2830
2831 let char_str = &line_text[current_byte_offset..current_byte_offset + c.len_utf8()];
2832 let char_width = measure_text_impl(char_str, style, font_size, font.raster_ref())
2833 .width
2834 .max(font_size * 0.5);
2835
2836 let left_dist = (x - glyph_x).abs();
2837 if left_dist < best_distance {
2838 best_distance = left_dist;
2839 best_offset = current_byte_offset;
2840 }
2841
2842 let right_x = glyph_x + char_width;
2843 let right_dist = (x - right_x).abs();
2844 if right_dist < best_distance {
2845 best_distance = right_dist;
2846 best_offset = current_byte_offset + c.len_utf8();
2847 }
2848
2849 current_byte_offset += c.len_utf8();
2850 }
2851
2852 let total_width = measure_text_impl(line_text, style, font_size, font.raster_ref()).width;
2853 let end_dist = (x - total_width).abs();
2854 if end_dist < best_distance {
2855 best_offset = line_text.len();
2856 }
2857
2858 line_start_byte + best_offset.min(line_text.len())
2859}
2860
2861pub fn cursor_x_for_offset_with_font(
2864 text: &str,
2865 style: &TextStyle,
2866 offset: usize,
2867 font: &SoftwareTextFont,
2868) -> f32 {
2869 if requires_font_layout(text, style) {
2870 return annotated_cursor_x_for_offset(
2871 StyledTextRef {
2872 text,
2873 span_styles: &[],
2874 },
2875 style,
2876 offset,
2877 font,
2878 );
2879 }
2880 let clamped_offset = clamp_to_char_boundary(text, offset.min(text.len()));
2881 if clamped_offset == 0 {
2882 return 0.0;
2883 }
2884
2885 let font_size = resolve_font_size(style);
2886 let shaped = font.shaped_for(style);
2887 let font = &*shaped;
2888 measure_text_impl(&text[..clamped_offset], style, font_size, font.raster_ref()).width
2889}
2890
2891pub fn layout_text_with_font(
2894 text: &str,
2895 style: &TextStyle,
2896 font: &SoftwareTextFont,
2897) -> TextLayoutResult {
2898 if requires_font_layout(text, style) {
2899 return layout_annotated_text_with_font_set(
2900 StyledTextRef {
2901 text,
2902 span_styles: &[],
2903 },
2904 style,
2905 font,
2906 );
2907 }
2908 let shaped = font.shaped_for(style);
2909 let font = &*shaped;
2910 let font_size = resolve_font_size(style);
2911 let glyph_font_size = font.ab_glyph_px_size(font_size);
2912 let resolved_weight = font.weight();
2913 let font_ref = font.raster_ref();
2914 let letter_spacing = font.metadata.tracking.resolve(style, font_size);
2915 let weight_synthesis = TextWeightSynthesis::for_style(style, resolved_weight, font_size, 1.0);
2916 let line_box = font_line_box(style, font, font_size);
2917 let line_height = line_box.height;
2918 let font = &font.font;
2919 let scaled_font = font.as_scaled(PxScale::from(glyph_font_size));
2920
2921 let mut glyph_x_positions = Vec::new();
2922 let mut char_to_byte = Vec::new();
2923 let mut glyph_layouts = Vec::new();
2924 let mut lines = Vec::new();
2925 let mut current_x = 0.0f32;
2926 let mut line_start = 0;
2927 let mut y = -line_box.trim_top;
2928
2929 let mut iter = text.char_indices().peekable();
2930 while let Some((byte_offset, c)) = iter.next() {
2931 glyph_x_positions.push(current_x);
2932 char_to_byte.push(byte_offset);
2933
2934 if c == '\n' {
2935 lines.push(LineLayout {
2936 start_offset: line_start,
2937 end_offset: byte_offset,
2938 y,
2939 height: line_height,
2940 });
2941 line_start = byte_offset + 1;
2942 y += line_height;
2943 current_x = 0.0;
2944 } else {
2945 let glyph_id = scaled_font.glyph_id(c);
2946 let glyph_width =
2947 weight_synthesis.apply_width(scaled_font.h_advance(glyph_id).max(0.0));
2948 let glyph_end = byte_offset + c.len_utf8();
2949 if glyph_end > byte_offset {
2950 glyph_layouts.push(GlyphLayout {
2951 line_index: lines.len(),
2952 start_offset: byte_offset,
2953 end_offset: glyph_end,
2954 x: current_x,
2955 y,
2956 width: glyph_width,
2957 height: line_height,
2958 });
2959 }
2960 current_x += glyph_width;
2961 if let Some((_, next)) = iter.peek()
2962 && *next != '\n'
2963 {
2964 current_x += letter_spacing;
2965 }
2966 }
2967 }
2968
2969 glyph_x_positions.push(current_x);
2970 char_to_byte.push(text.len());
2971
2972 lines.push(LineLayout {
2973 start_offset: line_start,
2974 end_offset: text.len(),
2975 y,
2976 height: line_height,
2977 });
2978
2979 let metrics = measure_text_impl(text, style, font_size, font_ref);
2980 TextLayoutResult::new(
2981 text,
2982 TextLayoutData {
2983 width: metrics.width,
2984 height: metrics.height,
2985 line_height,
2986 glyph_x_positions,
2987 char_to_byte,
2988 lines,
2989 glyph_layouts,
2990 },
2991 )
2992}
2993
2994fn walk_annotated_glyphs(
2995 segment: &AnnotatedTextSegment<'_>,
2996 mut visit: impl FnMut(GlyphLayout),
2997) -> f32 {
2998 #[cfg(feature = "text-shaping")]
2999 if requires_font_layout(segment.text, segment.style)
3000 && let Some(run) = segment.font.shaped_run(segment.text, segment.style)
3001 {
3002 let font = segment.font;
3003 let weight =
3004 TextWeightSynthesis::for_style(segment.style, font.weight(), segment.font_size, 1.0);
3005 let natural = font
3006 .font
3007 .as_scaled(font.ab_glyph_px_size(segment.font_size))
3008 .h_scale_factor();
3009 let scale = run.horizontal_scale(natural, weight.apply_width(natural));
3010 let spacing = run.spacing(
3011 font.metadata
3012 .tracking
3013 .resolve(segment.style, segment.font_size),
3014 );
3015 for (index, cluster) in run.clusters.iter().enumerate() {
3016 visit(GlyphLayout {
3017 line_index: segment.line_index,
3018 start_offset: segment.range.start + cluster.range.start,
3019 end_offset: segment.range.start + cluster.range.end,
3020 x: segment.origin.x + cluster.x * scale + (index as f32 + 0.5) * spacing,
3021 y: segment.line_top,
3022 width: cluster.advance * scale,
3023 height: segment.line_height,
3024 });
3025 }
3026 return run.width(scale, spacing);
3027 }
3028 let shaped = segment.font.shaped_for(segment.style);
3029 let font = &*shaped;
3030 let weight =
3031 TextWeightSynthesis::for_style(segment.style, font.weight(), segment.font_size, 1.0);
3032 let glyph_size = font.ab_glyph_px_size(segment.font_size);
3033 let scaled = font.font.as_scaled(PxScale {
3034 x: weight.apply_width(glyph_size),
3035 y: glyph_size,
3036 });
3037 let spacing = font
3038 .metadata
3039 .tracking
3040 .resolve(segment.style, segment.font_size);
3041 let lead = run_lead_in(segment.text, spacing);
3042 let mut x = segment.origin.x + lead;
3043 let mut previous = None;
3044 for (offset, ch) in segment.text.char_indices() {
3045 let glyph = scaled.glyph_id(ch);
3046 if let Some(previous) = previous {
3047 x += scaled.kern(previous, glyph) + spacing;
3048 }
3049 let width = scaled.h_advance(glyph);
3050 visit(GlyphLayout {
3051 line_index: segment.line_index,
3052 start_offset: segment.range.start + offset,
3053 end_offset: segment.range.start + offset + ch.len_utf8(),
3054 x,
3055 y: segment.line_top,
3056 width,
3057 height: segment.line_height,
3058 });
3059 x += width;
3060 previous = Some(glyph);
3061 }
3062 (x - segment.origin.x + lead).max(0.0)
3063}
3064
3065fn segment_is_rtl(segment: &AnnotatedTextSegment<'_>) -> bool {
3066 requires_font_layout(segment.text, segment.style)
3067 && segment
3068 .style
3069 .paragraph_style
3070 .text_direction
3071 .resolve(segment.text)
3072 == cranpose_ui::text::ResolvedTextDirection::Rtl
3073}
3074
3075fn cursor_edges(glyph: &GlyphLayout, rtl: bool) -> [(f32, usize); 2] {
3076 let (start, end) = if rtl {
3077 (glyph.x + glyph.width, glyph.x)
3078 } else {
3079 (glyph.x, glyph.x + glyph.width)
3080 };
3081 [(start, glyph.start_offset), (end, glyph.end_offset)]
3082}
3083
3084pub(crate) fn layout_annotated_text_with_font_set<'a>(
3085 text: impl Into<StyledTextRef<'a>>,
3086 style: &TextStyle,
3087 fonts: impl Into<FontResolver<'a>>,
3088) -> TextLayoutResult {
3089 let text = text.into();
3090 let fonts = fonts.into();
3091 if text.span_styles.is_empty()
3092 && !requires_font_layout(text.text, style)
3093 && let Some(font) = fonts.single_font_for_text(text.text, style)
3094 {
3095 return layout_text_with_font(text.text, style, font);
3096 }
3097 let Some(layout) = AnnotatedTextLayout::new(
3098 text,
3099 style,
3100 resolve_font_size(style),
3101 1.0,
3102 measure_grid(),
3103 fonts,
3104 ) else {
3105 return fallback_layout_text(text.text, style);
3106 };
3107 let char_to_byte: Vec<usize> = text
3108 .text
3109 .char_indices()
3110 .map(|(offset, _)| offset)
3111 .chain(std::iter::once(text.text.len()))
3112 .collect();
3113 let mut glyph_x_positions = vec![0.0; char_to_byte.len()];
3114 let mut glyph_layouts = Vec::new();
3115 let mut lines = Vec::with_capacity(layout.lines.len());
3116 let mut width = 0.0_f32;
3117 for (index, line) in layout.lines.iter().enumerate() {
3118 let mut end_cursor = None;
3119 let advance = layout
3120 .walk_line(index, 0.0, |segment| {
3121 let rtl = segment_is_rtl(&segment);
3122 Some(walk_annotated_glyphs(&segment, |glyph| {
3123 let [(start, offset), (end, end_offset)] = cursor_edges(&glyph, rtl);
3124 let first = char_to_byte.partition_point(|byte| *byte < offset);
3125 let last = char_to_byte.partition_point(|byte| *byte < end_offset);
3126 glyph_x_positions[first..last].fill(start);
3127 if end_offset == line.range.end {
3128 end_cursor = Some(end);
3129 }
3130 glyph_layouts.push(glyph);
3131 }))
3132 })
3133 .unwrap_or(0.0);
3134 width = width.max(advance);
3135 if let Ok(index) = char_to_byte.binary_search(&line.range.end) {
3136 glyph_x_positions[index] = end_cursor.unwrap_or(advance);
3137 }
3138 lines.push(LineLayout {
3139 start_offset: line.range.start,
3140 end_offset: line.range.end,
3141 y: line.top,
3142 height: line.line_box.height,
3143 });
3144 }
3145 let metrics = layout.metrics(width);
3146 TextLayoutResult::new(
3147 text.text,
3148 TextLayoutData {
3149 width: metrics.width,
3150 height: metrics.height,
3151 line_height: metrics.line_height,
3152 glyph_x_positions,
3153 char_to_byte,
3154 lines,
3155 glyph_layouts,
3156 },
3157 )
3158}
3159
3160pub(crate) fn annotated_cursor_x_for_offset<'a>(
3161 text: impl Into<StyledTextRef<'a>>,
3162 style: &TextStyle,
3163 offset: usize,
3164 fonts: impl Into<FontResolver<'a>>,
3165) -> f32 {
3166 let text = text.into();
3167 let fonts = fonts.into();
3168 let offset = clamp_to_char_boundary(text.text, offset);
3169 if text.span_styles.is_empty()
3170 && !requires_font_layout(text.text, style)
3171 && let Some(font) = fonts.single_font_for_text(text.text, style)
3172 {
3173 return cursor_x_for_offset_with_font(text.text, style, offset, font);
3174 }
3175 let Some(layout) = AnnotatedTextLayout::new(
3176 text,
3177 style,
3178 resolve_font_size(style),
3179 1.0,
3180 measure_grid(),
3181 fonts,
3182 ) else {
3183 return fallback_cursor_x_for_offset(text.text, style, offset);
3184 };
3185 let line_index = layout
3186 .lines
3187 .iter()
3188 .position(|line| offset <= line.range.end)
3189 .unwrap_or_else(|| layout.lines.len().saturating_sub(1));
3190 let mut x = 0.0;
3191 let mut best = (0, false);
3192 let advance = layout
3193 .walk_line(line_index, 0.0, |segment| {
3194 let rtl = segment_is_rtl(&segment);
3195 Some(walk_annotated_glyphs(&segment, |glyph| {
3196 for (index, (position, boundary)) in
3197 cursor_edges(&glyph, rtl).into_iter().enumerate()
3198 {
3199 let rank = (boundary, index == 0);
3200 if boundary <= offset && rank >= best {
3201 best = rank;
3202 x = position;
3203 }
3204 }
3205 }))
3206 })
3207 .unwrap_or(0.0);
3208 if !requires_font_layout(text.text, style)
3209 && layout
3210 .lines
3211 .get(line_index)
3212 .is_some_and(|line| offset >= line.range.end)
3213 {
3214 advance
3215 } else {
3216 x
3217 }
3218}
3219
3220pub(crate) fn annotated_offset_for_position<'a>(
3221 text: impl Into<StyledTextRef<'a>>,
3222 style: &TextStyle,
3223 x: f32,
3224 y: f32,
3225 fonts: impl Into<FontResolver<'a>>,
3226) -> usize {
3227 let text = text.into();
3228 let fonts = fonts.into();
3229 if text.span_styles.is_empty()
3230 && !requires_font_layout(text.text, style)
3231 && let Some(font) = fonts.single_font_for_text(text.text, style)
3232 {
3233 return text_offset_for_position_with_font(text.text, style, x, y, font);
3234 }
3235 let Some(layout) = AnnotatedTextLayout::new(
3236 text,
3237 style,
3238 resolve_font_size(style),
3239 1.0,
3240 measure_grid(),
3241 fonts,
3242 ) else {
3243 return fallback_text_offset_for_position(text.text, style, x, y);
3244 };
3245 let line_index = layout
3246 .lines
3247 .iter()
3248 .position(|line| y < line.top + line.line_box.height)
3249 .unwrap_or_else(|| layout.lines.len().saturating_sub(1));
3250 let Some(line) = layout.lines.get(line_index) else {
3251 return 0;
3252 };
3253 let mut best = line.range.start;
3254 let mut distance = f32::INFINITY;
3255 let advance = layout
3256 .walk_line(line_index, 0.0, |segment| {
3257 let rtl = segment_is_rtl(&segment);
3258 Some(walk_annotated_glyphs(&segment, |glyph| {
3259 for (position, offset) in cursor_edges(&glyph, rtl) {
3260 let delta = (x - position).abs();
3261 if delta < distance {
3262 distance = delta;
3263 best = offset;
3264 }
3265 }
3266 }))
3267 })
3268 .unwrap_or(0.0);
3269 if !requires_font_layout(text.text, style) && (x - advance).abs() < distance {
3270 line.range.end
3271 } else {
3272 best
3273 }
3274}
3275
3276pub fn rasterize_text_to_image_with_font(
3279 text: &str,
3280 rect: Rect,
3281 style: &TextStyle,
3282 fallback_color: Color,
3283 font_size: f32,
3284 scale: f32,
3285 font: &impl Font,
3286) -> Option<ImageBitmap> {
3287 rasterize_text_to_image_impl(
3288 TextRasterImageRequest {
3289 text,
3290 rect,
3291 style,
3292 fallback_color,
3293 font_size,
3294 scale,
3295 },
3296 RasterFontRef {
3297 font,
3298 ab_glyph_scale_factor: 1.0,
3299 weight: FontWeight::NORMAL,
3300 style: FontStyle::Normal,
3301 tracking: &FontTracking::default(),
3302 },
3303 0,
3304 None,
3305 )
3306 .and_then(TextPixels::into_image)
3307}
3308
3309struct GlyphPaint<'a> {
3310 brush: &'a Brush,
3311 alpha: f32,
3312 shadow: Option<Shadow>,
3313 brush_rect: Rect,
3314 canvas_origin: Point,
3315 scale: f32,
3316 static_text_motion: bool,
3317}
3318
3319struct GlyphRasterClip {
3320 width: f32,
3321 height: f32,
3322 shadow: Option<(Point, f32)>,
3323}
3324
3325impl GlyphRasterClip {
3326 fn new(width: u32, height: u32, paint: &GlyphPaint<'_>) -> Self {
3327 Self {
3328 width: width as f32,
3329 height: height as f32,
3330 shadow: paint.shadow.map(|shadow| {
3331 (
3332 Point::new(shadow.offset.x * paint.scale, shadow.offset.y * paint.scale),
3333 whole::ceil(
3334 shadow_blur_sigma((shadow.blur_radius * paint.scale).max(0.0)) * 3.0,
3335 ) + 1.0,
3336 )
3337 }),
3338 }
3339 }
3340
3341 fn intersects(&self, bounds: Rect) -> bool {
3342 let overlaps = |rect: Rect| {
3343 rect.x < self.width
3344 && rect.y < self.height
3345 && rect.x + rect.width > 0.0
3346 && rect.y + rect.height > 0.0
3347 };
3348 overlaps(bounds)
3349 || self.shadow.is_some_and(|(offset, margin)| {
3350 overlaps(Rect {
3351 x: bounds.x + offset.x - margin,
3352 y: bounds.y + offset.y - margin,
3353 width: bounds.width + 2.0 * margin,
3354 height: bounds.height + 2.0 * margin,
3355 })
3356 })
3357 }
3358}
3359
3360enum TextRasterPixels {
3361 Solid(Vec<u8>),
3362 Blended(Vec<[f32; 4]>),
3363}
3364
3365struct TextRasterCanvas {
3366 width: u32,
3367 height: u32,
3368 pixels: TextRasterPixels,
3369}
3370
3371impl TextRasterCanvas {
3372 fn new(width: u32, height: u32, solid: bool) -> Self {
3373 let len = width as usize * height as usize;
3374 Self {
3375 width,
3376 height,
3377 pixels: if solid {
3378 TextRasterPixels::Solid(vec![0; len * 4])
3379 } else {
3380 TextRasterPixels::Blended(vec![[0.0; 4]; len])
3381 },
3382 }
3383 }
3384
3385 fn paint(&mut self, mask: &GlyphMask, paint: &GlyphPaint<'_>) {
3386 match &mut self.pixels {
3387 TextRasterPixels::Solid(pixels) => {
3388 if let Brush::Solid(color) = paint.brush {
3389 draw_mask_glyph_solid_u8(
3390 pixels,
3391 self.width,
3392 self.height,
3393 mask,
3394 color_to_rgba(*color),
3395 paint.alpha,
3396 );
3397 }
3398 }
3399 TextRasterPixels::Blended(pixels) => {
3400 if let Some(shadow) = paint.shadow {
3401 draw_shadow_mask(
3402 pixels,
3403 self.width,
3404 self.height,
3405 mask,
3406 shadow,
3407 paint.scale,
3408 paint.static_text_motion,
3409 );
3410 }
3411 draw_mask_glyph(pixels, self.width, self.height, mask, paint);
3412 }
3413 }
3414 }
3415
3416 fn into_pixels(self) -> Option<TextPixels> {
3417 if self.width == 0 || self.height == 0 {
3418 return None;
3419 }
3420 let pixels = match self.pixels {
3421 TextRasterPixels::Solid(pixels) => pixels,
3422 TextRasterPixels::Blended(canvas) => {
3423 let mut pixels = Vec::with_capacity(canvas.len() * 4);
3424 for pixel in canvas {
3425 pixels.extend(
3426 pixel.map(|channel| whole::round(channel.clamp(0.0, 1.0) * 255.0) as u8),
3427 );
3428 }
3429 pixels
3430 }
3431 };
3432 Some(TextPixels {
3433 width: self.width,
3434 height: self.height,
3435 pixels,
3436 })
3437 }
3438}
3439
3440struct TextRasterImageRequest<'a> {
3441 text: &'a str,
3442 rect: Rect,
3443 style: &'a TextStyle,
3444 fallback_color: Color,
3445 font_size: f32,
3446 scale: f32,
3447}
3448
3449fn rasterize_text_to_image_impl(
3450 request: TextRasterImageRequest<'_>,
3451 font_ref: RasterFontRef<'_, impl Font>,
3452 font_cache_key: u64,
3453 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
3454) -> Option<TextPixels> {
3455 let TextRasterImageRequest {
3456 text,
3457 rect,
3458 style,
3459 fallback_color,
3460 font_size,
3461 scale,
3462 } = request;
3463
3464 if text_render_request_is_degenerate(text.is_empty(), rect, font_size, scale) {
3465 return None;
3466 }
3467
3468 let width = whole::ceil(rect.width).max(1.0) as u32;
3469 let height = whole::ceil(rect.height).max(1.0) as u32;
3470
3471 let fallback_brush = Brush::solid(fallback_color);
3472 let (brush, brush_alpha_multiplier) = match style.span_style.brush.as_ref() {
3473 Some(brush) => (brush, style.span_style.alpha.unwrap_or(1.0).clamp(0.0, 1.0)),
3474 None => (&fallback_brush, 1.0),
3475 };
3476 let raster_style = match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
3477 TextDrawStyle::Fill => GlyphRasterStyle::Fill,
3478 TextDrawStyle::Stroke { width } => {
3479 if width.is_finite() && width > 0.0 {
3480 GlyphRasterStyle::Stroke {
3481 width_px: width * scale,
3482 }
3483 } else {
3484 GlyphRasterStyle::Fill
3485 }
3486 }
3487 };
3488 let shadow = style
3489 .span_style
3490 .shadow
3491 .filter(|shadow| shadow.color.3 > 0.0);
3492 let static_text_motion = style
3493 .paragraph_style
3494 .text_motion
3495 .unwrap_or(TextMotion::Static)
3496 == TextMotion::Static;
3497
3498 let origin_x = if static_text_motion {
3499 0.0
3500 } else {
3501 rect.x.fract()
3502 };
3503 let origin_y = if static_text_motion {
3504 0.0
3505 } else {
3506 rect.y.fract()
3507 };
3508
3509 let font = font_ref.font;
3510 let font_px_size = font_size * scale * font_ref.ab_glyph_scale_factor;
3511 let letter_spacing = font_ref.tracking.resolve(style, font_size) * scale;
3512 let align_fraction = cranpose_ui::text::text_align_fraction(style, text);
3513 let weight_synthesis = TextWeightSynthesis::for_style(style, font_ref.weight, font_size, scale);
3514 let style_synthesis = TextStyleSynthesis::for_style(style, font_ref.style, font_size, scale);
3515 let metrics = vertical_metrics(font, font_px_size);
3516 let line_box = line_box_for(style, metrics, asked_line_height(style, scale), 1.0);
3517 let line_height = line_box.height;
3518 let first_baseline_y = line_box.first_baseline();
3519
3520 let mut canvas = TextRasterCanvas::new(
3521 width,
3522 height,
3523 matches!(brush, Brush::Solid(_)) && shadow.is_none(),
3524 );
3525 let paint = GlyphPaint {
3526 brush,
3527 alpha: brush_alpha_multiplier,
3528 shadow,
3529 brush_rect: rect,
3530 canvas_origin: Point::new(rect.x, rect.y),
3531 scale,
3532 static_text_motion,
3533 };
3534 visit_text_glyph_masks(
3535 text,
3536 font,
3537 font_cache_key,
3538 font_px_size,
3539 line_height,
3540 first_baseline_y,
3541 origin_x,
3542 origin_y,
3543 letter_spacing,
3544 align_fraction,
3545 static_text_motion,
3546 raster_style,
3547 weight_synthesis,
3548 style_synthesis,
3549 glyph_cache,
3550 None,
3551 |mask| canvas.paint(mask, &paint),
3552 );
3553 canvas.into_pixels()
3554}
3555
3556#[expect(clippy::too_many_arguments)]
3557fn visit_text_segment_masks(
3558 text: &str,
3559 local_rect: Rect,
3560 style: &TextStyle,
3561 font_size: f32,
3562 scale: f32,
3563 font: &SoftwareTextFont,
3564 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
3565 clip: &GlyphRasterClip,
3566 visit: impl FnMut(&GlyphMask),
3567) -> f32 {
3568 if text_render_request_is_degenerate(text.is_empty(), local_rect, font_size, scale) {
3569 return 0.0;
3570 }
3571 let raster_style = match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
3572 TextDrawStyle::Stroke { width } if width.is_finite() && width > 0.0 => {
3573 GlyphRasterStyle::Stroke {
3574 width_px: width * scale,
3575 }
3576 }
3577 _ => GlyphRasterStyle::Fill,
3578 };
3579 let static_text_motion = style
3580 .paragraph_style
3581 .text_motion
3582 .unwrap_or(TextMotion::Static)
3583 == TextMotion::Static;
3584 let shaped = font.shaped_for(style);
3585 let font = &*shaped;
3586 let m = text_segment_metrics(text, local_rect, style, font_size, scale, font);
3587 #[cfg(feature = "text-shaping")]
3588 if requires_font_layout(text, style) {
3589 let mut glyph_cache = glyph_cache;
3590 let mut visit = visit;
3591 return visit_shaped_glyphs(text, style, font, &m, local_rect.x, |glyph| {
3592 let glyph = align_glyph_for_text_motion(glyph, static_text_motion);
3593 if let Some(mask) = raster_glyph_mask(
3594 if static_text_motion {
3595 glyph_cache.as_deref_mut()
3596 } else {
3597 None
3598 },
3599 font.content_hash(),
3600 &font.font,
3601 &glyph,
3602 raster_style,
3603 m.weight_synthesis,
3604 m.style_synthesis,
3605 Some(clip),
3606 ) {
3607 visit(&mask);
3608 }
3609 ControlFlow::Continue(())
3610 })
3611 .unwrap_or(0.0);
3612 }
3613 visit_text_glyph_masks(
3614 text,
3615 &font.font,
3616 font.content_hash(),
3617 m.font_px_size,
3618 m.line_height,
3619 m.first_baseline_y,
3620 local_rect.x,
3621 0.0,
3622 m.letter_spacing,
3623 m.align_fraction,
3624 static_text_motion,
3625 raster_style,
3626 m.weight_synthesis,
3627 m.style_synthesis,
3628 glyph_cache,
3629 Some(clip),
3630 visit,
3631 )
3632}
3633
3634#[expect(clippy::too_many_arguments)]
3635fn collect_text_segment_solid_atlas_glyphs(
3636 text: &str,
3637 local_rect: Rect,
3638 style: &TextStyle,
3639 color: Color,
3640 font_size: f32,
3641 scale: f32,
3642 font: &SoftwareTextFont,
3643 glyph_cache: &mut SoftwareGlyphRasterCache,
3644 out: &mut Vec<SoftwareGlyphAtlasGlyph>,
3645) -> Option<f32> {
3646 let request = AtlasSegmentRequest {
3647 text,
3648 local_rect,
3649 style,
3650 font_size,
3651 scale,
3652 font,
3653 };
3654 collect_atlas_segment(request, glyph_cache, out, |cache, glyph, out| {
3655 let mask = match cache.get(&glyph.key, &glyph.glyph) {
3656 Some(mask) => mask,
3657 None => {
3658 let Some(mask) = glyph.build_mask() else {
3659 return ControlFlow::Continue(());
3660 };
3661 cache.put(glyph.key, &glyph.glyph, mask)
3662 }
3663 };
3664 if let Some(key) = glyph_atlas_key_from_mask_key(glyph.key)
3665 && mask.width != 0
3666 && mask.height != 0
3667 {
3668 out.push(SoftwareGlyphAtlasGlyph {
3669 key,
3670 mask: SoftwareGlyphAtlasMask {
3671 alpha: mask.alpha,
3672 width: mask.width,
3673 height: mask.height,
3674 },
3675 x: mask.origin_x,
3676 y: mask.origin_y,
3677 color,
3678 });
3679 }
3680 ControlFlow::Continue(())
3681 })
3682}
3683
3684#[expect(clippy::too_many_arguments)]
3685fn collect_text_segment_cached_solid_atlas_placements(
3686 text: &str,
3687 local_rect: Rect,
3688 style: &TextStyle,
3689 color: Color,
3690 font_size: f32,
3691 scale: f32,
3692 font: &SoftwareTextFont,
3693 glyph_cache: &mut SoftwareGlyphRasterCache,
3694 out: &mut Vec<SoftwareGlyphAtlasPlacement>,
3695) -> Option<f32> {
3696 let request = AtlasSegmentRequest {
3697 text,
3698 local_rect,
3699 style,
3700 font_size,
3701 scale,
3702 font,
3703 };
3704 collect_atlas_segment(request, glyph_cache, out, |cache, glyph, out| {
3705 let Some((key, x, y, width, height)) = cache.get_atlas_placement(&glyph.key, &glyph.glyph)
3706 else {
3707 return if glyph.has_outline() {
3710 ControlFlow::Break(())
3711 } else {
3712 ControlFlow::Continue(())
3713 };
3714 };
3715 if width != 0 && height != 0 {
3716 out.push(SoftwareGlyphAtlasPlacement {
3717 key,
3718 x,
3719 y,
3720 width,
3721 height,
3722 color,
3723 });
3724 }
3725 ControlFlow::Continue(())
3726 })
3727}
3728
3729#[expect(clippy::too_many_arguments)]
3730fn collect_text_segment_solid_atlas_run(
3731 text: &str,
3732 local_rect: Rect,
3733 style: &TextStyle,
3734 color: Color,
3735 font_size: f32,
3736 scale: f32,
3737 font: &SoftwareTextFont,
3738 glyph_cache: &mut SoftwareGlyphRasterCache,
3739 out: &mut Vec<SoftwareGlyphAtlasRunGlyph>,
3740) -> Option<f32> {
3741 let request = AtlasSegmentRequest {
3742 text,
3743 local_rect,
3744 style,
3745 font_size,
3746 scale,
3747 font,
3748 };
3749 glyph_cache.segment_metrics.clear();
3750 let transparent = color.3 <= 0.0;
3751 collect_atlas_segment(request, glyph_cache, out, |cache, glyph, out| {
3752 if transparent {
3753 return ControlFlow::Continue(());
3754 }
3755 let metrics = match cache.segment_metrics.get(glyph.ch) {
3756 Some(metrics) => metrics,
3757 None => match cache.get_atlas_metrics(&glyph.key) {
3758 Some(metrics) => {
3759 cache.segment_metrics.insert(glyph.ch, metrics);
3760 metrics
3761 }
3762 None => {
3763 if let Some(new) = rasterize_atlas_run_glyph(cache, glyph)
3764 && new.mask.width != 0
3765 && new.mask.height != 0
3766 {
3767 out.push(SoftwareGlyphAtlasRunGlyph::New(SoftwareGlyphAtlasGlyph {
3768 color,
3769 ..new
3770 }));
3771 }
3772 return ControlFlow::Continue(());
3773 }
3774 },
3775 };
3776 if metrics.width != 0 && metrics.height != 0 {
3777 out.push(SoftwareGlyphAtlasRunGlyph::Cached(
3778 metrics.placement(&glyph.glyph, color),
3779 ));
3780 }
3781 ControlFlow::Continue(())
3782 })
3783}
3784
3785fn rasterize_atlas_run_glyph(
3788 cache: &mut SoftwareGlyphRasterCache,
3789 glyph: &AtlasSegmentGlyph<'_>,
3790) -> Option<SoftwareGlyphAtlasGlyph> {
3791 if !glyph.has_outline() {
3792 return None;
3793 }
3794 let mask = glyph.build_mask()?;
3795 let mask = cache.put(glyph.key, &glyph.glyph, mask);
3796 let key = glyph_atlas_key_from_mask_key(glyph.key)?;
3797 let (glyph_x, glyph_y) = static_glyph_pixel_origin(&glyph.glyph);
3798 cache.segment_metrics.insert(
3799 glyph.ch,
3800 CachedAtlasGlyphMetrics {
3801 key,
3802 width: mask.width,
3803 height: mask.height,
3804 origin_offset_x: mask.origin_x - glyph_x,
3805 origin_offset_y: mask.origin_y - glyph_y,
3806 },
3807 );
3808 Some(SoftwareGlyphAtlasGlyph {
3809 key,
3810 mask: SoftwareGlyphAtlasMask {
3811 alpha: mask.alpha,
3812 width: mask.width,
3813 height: mask.height,
3814 },
3815 x: mask.origin_x,
3816 y: mask.origin_y,
3817 color: Color::WHITE,
3818 })
3819}
3820
3821#[derive(Clone, Copy)]
3823struct AtlasSegmentRequest<'a> {
3824 text: &'a str,
3825 local_rect: Rect,
3826 style: &'a TextStyle,
3827 font_size: f32,
3828 scale: f32,
3829 font: &'a SoftwareTextFont,
3830}
3831
3832struct AtlasSegmentGlyph<'a> {
3835 ch: char,
3836 glyph: Glyph,
3837 key: GlyphMaskCacheKey,
3838 font: &'a KernedFont,
3839 metrics: &'a TextSegmentMetrics,
3840}
3841
3842impl AtlasSegmentGlyph<'_> {
3843 fn has_outline(&self) -> bool {
3844 self.font.outline(self.glyph.id).is_some()
3845 }
3846
3847 fn build_mask(&self) -> Option<GlyphMask> {
3848 build_complete_glyph_mask(
3849 self.font,
3850 &self.glyph,
3851 GlyphRasterStyle::Fill,
3852 self.metrics.weight_synthesis,
3853 self.metrics.style_synthesis,
3854 None,
3855 )
3856 }
3857}
3858
3859impl<'a> AtlasSegmentGlyph<'a> {
3860 fn new(
3861 ch: char,
3862 glyph: Glyph,
3863 font: &'a SoftwareTextFont,
3864 metrics: &'a TextSegmentMetrics,
3865 ) -> Self {
3866 let glyph = align_glyph_for_text_motion(glyph, true);
3867 Self {
3868 ch,
3869 key: glyph_mask_cache_key(
3870 font.content_hash(),
3871 &glyph,
3872 GlyphRasterStyle::Fill,
3873 metrics.weight_synthesis,
3874 metrics.style_synthesis,
3875 ),
3876 glyph,
3877 font: &font.font,
3878 metrics,
3879 }
3880 }
3881}
3882
3883fn collect_atlas_segment<T>(
3889 request: AtlasSegmentRequest<'_>,
3890 glyph_cache: &mut SoftwareGlyphRasterCache,
3891 out: &mut Vec<T>,
3892 mut visit: impl FnMut(
3893 &mut SoftwareGlyphRasterCache,
3894 &AtlasSegmentGlyph<'_>,
3895 &mut Vec<T>,
3896 ) -> ControlFlow<()>,
3897) -> Option<f32> {
3898 let AtlasSegmentRequest {
3899 text,
3900 local_rect,
3901 style,
3902 font_size,
3903 scale,
3904 font,
3905 } = request;
3906 if text_render_request_is_degenerate(text.is_empty(), local_rect, font_size, scale) {
3907 return Some(0.0);
3908 }
3909 if !text_segment_supports_solid_atlas(style) {
3910 return None;
3911 }
3912
3913 let shaped = font.shaped_for(style);
3914 let font = &*shaped;
3915 let m = text_segment_metrics(text, local_rect, style, font_size, scale, font);
3916 let font_hash = font.content_hash();
3917 let kerned = &font.font;
3918 let origin_x = whole::round(local_rect.x);
3919 let initial_len = out.len();
3920 #[cfg(feature = "text-shaping")]
3921 if requires_font_layout(text, style) {
3922 let result = visit_shaped_glyphs(text, style, font, &m, origin_x, |glyph| {
3923 let glyph = AtlasSegmentGlyph::new('\0', glyph, font, &m);
3924 visit(glyph_cache, &glyph, out)
3925 });
3926 if result.is_none() {
3927 out.truncate(initial_len);
3928 }
3929 return result;
3930 }
3931 let px_scale = PxScale::from(m.font_px_size);
3932 let scaled_font = kerned.as_scaled(px_scale);
3933 let h_scale = scaled_font.h_scale_factor();
3934 let line_offsets =
3935 line_alignment_offsets(&scaled_font, text, m.letter_spacing, m.align_fraction);
3936 let mut max_advance = 0.0f32;
3937 for (line_idx, line) in text.split('\n').enumerate() {
3938 let baseline_y = m.first_baseline_y + line_idx as f32 * m.line_height;
3939 let lead_in = run_lead_in(line, m.letter_spacing);
3940 let mut caret_x = origin_x + line_offset(&line_offsets, line_idx) + lead_in;
3941 let mut previous: Option<(char, GlyphId)> = None;
3942 for ch in line.chars() {
3943 let (glyph_id, advance) = kerned.glyph_advance(ch);
3944 if let Some(previous) = previous {
3945 let kern = kerned
3946 .ascii_kern(previous, (ch, glyph_id))
3947 .unwrap_or_else(|| {
3948 glyph_cache.kern_unscaled(font_hash, kerned, previous.1, glyph_id)
3949 });
3950 caret_x += kern * h_scale + m.letter_spacing;
3951 }
3952 let glyph = glyph_id.with_scale_and_position(px_scale, point(caret_x, baseline_y));
3953 caret_x += advance * h_scale;
3954 previous = Some((ch, glyph_id));
3955 let segment_glyph = AtlasSegmentGlyph::new(ch, glyph, font, &m);
3956 if visit(glyph_cache, &segment_glyph, out).is_break() {
3957 out.truncate(initial_len);
3958 return None;
3959 }
3960 }
3961 max_advance = max_advance.max((caret_x - origin_x + lead_in).max(0.0));
3962 }
3963 Some(max_advance)
3964}
3965
3966fn resolve_font_size(style: &TextStyle) -> f32 {
3967 style.resolve_font_size(14.0)
3968}
3969
3970#[cfg(feature = "text-shaping")]
3971fn shaped_run_width(
3972 run: &crate::text_shaping::ShapedRun,
3973 font: &SoftwareTextFont,
3974 style: &TextStyle,
3975 font_size: f32,
3976 scale: f32,
3977) -> f32 {
3978 let scaled = font
3979 .font
3980 .as_scaled(font.ab_glyph_px_size(font_size) * scale);
3981 let weight = TextWeightSynthesis::for_style(style, font.weight(), font_size, scale);
3982 run.width(
3983 run.horizontal_scale(
3984 scaled.h_scale_factor(),
3985 weight.apply_width(scaled.h_scale_factor()),
3986 ),
3987 font.metadata.tracking.resolve(style, font_size) * scale,
3988 )
3989}
3990
3991#[cfg(feature = "text-shaping")]
3992fn measure_shaped_text(
3993 text: &str,
3994 style: &TextStyle,
3995 font_size: f32,
3996 font: &SoftwareTextFont,
3997) -> TextMetrics {
3998 let line_box = font_line_box(style, font, font_size);
3999 let mut width = 0.0_f32;
4000 let mut count = 0;
4001 for line in text.split('\n') {
4002 count += 1;
4003 if let Some(run) = font.shaped_run(line, style) {
4004 let overhang = if line.is_empty() {
4005 0.0
4006 } else {
4007 TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0)
4008 .visual_overhang_px()
4009 };
4010 width = width.max(shaped_run_width(&run, font, style, font_size, 1.0) + overhang);
4011 }
4012 }
4013 TextMetrics {
4014 width,
4015 height: line_box.block_height(count),
4016 line_height: line_box.height,
4017 line_count: count,
4018 }
4019}
4020
4021#[cfg(feature = "text-shaping")]
4022fn visit_shaped_glyphs(
4023 text: &str,
4024 style: &TextStyle,
4025 font: &SoftwareTextFont,
4026 m: &TextSegmentMetrics,
4027 origin_x: f32,
4028 mut visit: impl FnMut(Glyph) -> ControlFlow<()>,
4029) -> Option<f32> {
4030 let scaled = font.font.as_scaled(m.font_px_size);
4031 let natural_scale = scaled.h_scale_factor();
4032 let synthesized_scale = m.weight_synthesis.apply_width(natural_scale);
4033 let scale_y = scaled.v_scale_factor();
4034 let mut width = 0.0_f32;
4035 for line in text.split('\n') {
4036 let run = font.shaped_run(line, style)?;
4037 width = width.max(run.width(
4038 run.horizontal_scale(natural_scale, synthesized_scale),
4039 m.letter_spacing,
4040 ));
4041 }
4042 for (index, line) in text.split('\n').enumerate() {
4043 let run = font.shaped_run(line, style)?;
4044 let scale_x = run.horizontal_scale(natural_scale, synthesized_scale);
4045 let x = origin_x + (width - run.width(scale_x, m.letter_spacing)) * m.align_fraction;
4046 let y = m.first_baseline_y + index as f32 * m.line_height;
4047 let mut result = ControlFlow::Continue(());
4048 run.visit(scale_x, scale_y, m.letter_spacing, |id, dx, dy| {
4049 if result.is_continue() {
4050 result = visit(id.with_scale_and_position(m.font_px_size, point(x + dx, y + dy)));
4051 }
4052 });
4053 if result.is_break() {
4054 return None;
4055 }
4056 }
4057 Some(width)
4058}
4059
4060fn line_box_for(
4061 style: &TextStyle,
4062 metrics: crate::font_layout::FontVerticalMetrics,
4063 line_height: f32,
4064 grid: f32,
4065) -> cranpose_ui::text::LineBox {
4066 cranpose_ui::text::line_box(
4067 style,
4068 cranpose_ui::text::FontExtent::new(metrics.ascent, -metrics.descent, metrics.line_gap),
4069 line_height,
4070 grid,
4071 )
4072}
4073
4074pub(crate) fn font_extent(
4075 font: &SoftwareTextFont,
4076 font_size: f32,
4077) -> cranpose_ui::text::FontExtent {
4078 let metrics = vertical_metrics(&font.font, font.ab_glyph_px_size(font_size));
4079 cranpose_ui::text::FontExtent::new(metrics.ascent, -metrics.descent, metrics.line_gap)
4080}
4081
4082pub(crate) fn font_line_box(
4084 style: &TextStyle,
4085 font: &SoftwareTextFont,
4086 font_size: f32,
4087) -> cranpose_ui::text::LineBox {
4088 line_box_for(
4089 style,
4090 crate::font_layout::vertical_metrics(&font.font, font.ab_glyph_px_size(font_size)),
4091 asked_line_height(style, 1.0),
4092 measure_grid(),
4093 )
4094}
4095
4096pub(crate) fn font_glyph_line_box(style: &TextStyle, font: &SoftwareTextFont) -> (f32, f32) {
4101 let font_size = resolve_font_size(style);
4102 let metrics =
4103 crate::font_layout::vertical_metrics(&font.font, font.ab_glyph_px_size(font_size));
4104 let resolved = font_line_box(style, font, font_size);
4105 let height = metrics.natural_line_height.min(resolved.height).max(1.0);
4106 (resolved.first_baseline() - metrics.ascent, height)
4107}
4108
4109fn measure_grid() -> f32 {
4110 if cranpose_ui::has_current_app_context() {
4111 cranpose_ui::current_density()
4112 } else {
4113 1.0
4114 }
4115}
4116
4117fn resolve_line_height(style: &TextStyle, font_size: f32) -> f32 {
4118 style.resolve_line_height(14.0, font_size)
4119}
4120
4121pub(crate) fn asked_line_height(style: &TextStyle, scale: f32) -> f32 {
4124 (style.resolve_line_height(14.0, f32::NAN) * scale).max(1.0)
4125}
4126
4127fn resolve_letter_spacing(style: &TextStyle, font_size: f32) -> f32 {
4128 let _ = font_size;
4129 style.resolve_letter_spacing(14.0)
4130}
4131
4132fn run_tracking(char_count: usize, letter_spacing: f32) -> f32 {
4133 char_count as f32 * letter_spacing
4134}
4135
4136fn run_lead_in(line: &str, letter_spacing: f32) -> f32 {
4137 if line.is_empty() {
4138 0.0
4139 } else {
4140 letter_spacing * 0.5
4141 }
4142}
4143
4144fn fallback_char_width(font_size: f32) -> f32 {
4145 font_size.max(1.0) * 0.55
4146}
4147
4148fn fallback_line_height(style: &TextStyle, font_size: f32) -> f32 {
4149 resolve_line_height(style, font_size.max(1.0) * 1.2)
4150}
4151
4152fn fallback_text_metrics(text: &str, style: &TextStyle, font_size: f32) -> TextMetrics {
4153 let line_height = fallback_line_height(style, font_size);
4154 let char_width = fallback_char_width(font_size);
4155 let letter_spacing = resolve_letter_spacing(style, font_size);
4156 let mut line_count = 0usize;
4157 let mut max_width = 0.0f32;
4158
4159 for line in text.split('\n') {
4160 line_count += 1;
4161 let char_count = line.chars().count();
4162 let spacing = run_tracking(char_count, letter_spacing);
4163 max_width = max_width.max(char_count as f32 * char_width + spacing);
4164 }
4165
4166 let line_count = line_count.max(1);
4167 TextMetrics {
4168 width: max_width,
4169 height: line_count as f32 * line_height,
4170 line_height,
4171 line_count,
4172 }
4173}
4174
4175fn fallback_cursor_x_for_offset(text: &str, style: &TextStyle, offset: usize) -> f32 {
4176 let font_size = resolve_font_size(style);
4177 let clamped = clamp_to_char_boundary(text, offset.min(text.len()));
4178 let line_start = text[..clamped].rfind('\n').map_or(0, |index| index + 1);
4179 let char_count = text[line_start..clamped].chars().count();
4180 let spacing = run_tracking(char_count, resolve_letter_spacing(style, font_size));
4181 char_count as f32 * fallback_char_width(font_size) + spacing
4182}
4183
4184fn fallback_text_offset_for_position(text: &str, style: &TextStyle, x: f32, y: f32) -> usize {
4185 if text.is_empty() {
4186 return 0;
4187 }
4188
4189 let font_size = resolve_font_size(style);
4190 let line_height = fallback_line_height(style, font_size);
4191 let line_index = whole::floor(y / line_height).max(0.0) as usize;
4192 let lines: Vec<&str> = text.split('\n').collect();
4193 let target_line = line_index.min(lines.len().saturating_sub(1));
4194
4195 let mut line_start_byte = 0;
4196 for line in lines.iter().take(target_line) {
4197 line_start_byte += line.len() + 1;
4198 }
4199
4200 let line_text = lines.get(target_line).copied().unwrap_or("");
4201 if line_text.is_empty() {
4202 return line_start_byte;
4203 }
4204
4205 let advance =
4206 (fallback_char_width(font_size) + resolve_letter_spacing(style, font_size)).max(1.0);
4207 let target_char = whole::round(x / advance).max(0.0) as usize;
4208 line_start_byte + byte_offset_for_char_index(line_text, target_char)
4209}
4210
4211fn fallback_layout_text(text: &str, style: &TextStyle) -> TextLayoutResult {
4212 let font_size = resolve_font_size(style);
4213 let line_height = fallback_line_height(style, font_size);
4214 let char_width = fallback_char_width(font_size);
4215 let letter_spacing = resolve_letter_spacing(style, font_size);
4216
4217 let mut glyph_x_positions = Vec::new();
4218 let mut char_to_byte = Vec::new();
4219 let mut glyph_layouts = Vec::new();
4220 let mut lines = Vec::new();
4221 let mut current_x = 0.0f32;
4222 let mut line_start = 0;
4223 let mut y = 0.0f32;
4224
4225 let mut iter = text.char_indices().peekable();
4226 while let Some((byte_offset, ch)) = iter.next() {
4227 glyph_x_positions.push(current_x);
4228 char_to_byte.push(byte_offset);
4229
4230 if ch == '\n' {
4231 lines.push(LineLayout {
4232 start_offset: line_start,
4233 end_offset: byte_offset,
4234 y,
4235 height: line_height,
4236 });
4237 line_start = byte_offset + 1;
4238 y += line_height;
4239 current_x = 0.0;
4240 } else {
4241 glyph_layouts.push(GlyphLayout {
4242 line_index: lines.len(),
4243 start_offset: byte_offset,
4244 end_offset: byte_offset + ch.len_utf8(),
4245 x: current_x,
4246 y,
4247 width: char_width,
4248 height: line_height,
4249 });
4250 current_x += char_width;
4251 if let Some((_, next)) = iter.peek()
4252 && *next != '\n'
4253 {
4254 current_x += letter_spacing;
4255 }
4256 }
4257 }
4258
4259 glyph_x_positions.push(current_x);
4260 char_to_byte.push(text.len());
4261 lines.push(LineLayout {
4262 start_offset: line_start,
4263 end_offset: text.len(),
4264 y,
4265 height: line_height,
4266 });
4267
4268 let metrics = fallback_text_metrics(text, style, font_size);
4269 TextLayoutResult::new(
4270 text,
4271 TextLayoutData {
4272 width: metrics.width,
4273 height: metrics.height,
4274 line_height,
4275 glyph_x_positions,
4276 char_to_byte,
4277 glyph_layouts,
4278 lines,
4279 },
4280 )
4281}
4282
4283fn style_allows_prefix_widths(style: &TextStyle) -> bool {
4284 !matches!(
4285 style
4286 .paragraph_style
4287 .platform_style
4288 .and_then(|platform| platform.shaping),
4289 Some(TextShaping::Advanced)
4290 )
4291}
4292
4293fn cached_line_advance_width(
4294 font: &SoftwareTextFont,
4295 text: &str,
4296 glyph_font_size: f32,
4297 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
4298) -> f32 {
4299 let scaled_font = font.font.as_scaled(PxScale::from(glyph_font_size));
4300 let h_scale = scaled_font.h_scale_factor();
4301 let mut width = 0.0f32;
4302 let mut previous = None;
4303
4304 for ch in text.chars() {
4305 let metrics = glyph_metrics.glyph_metrics(font, &scaled_font, ch);
4306 if let Some(previous) = previous {
4307 width +=
4308 glyph_metrics.kern(font, &scaled_font, previous, (ch, metrics.glyph_id)) * h_scale;
4309 }
4310 width += metrics.advance_unscaled * h_scale;
4311 previous = Some((ch, metrics.glyph_id));
4312 }
4313
4314 width.max(0.0)
4315}
4316
4317trait PrefixWidthSink {
4321 fn push(&mut self, separator: f32, prefix_width: f32);
4322}
4323
4324struct PrefixWidthVecs {
4327 prefix_widths: Vec<f32>,
4328 separator_before: Vec<f32>,
4329}
4330
4331impl PrefixWidthSink for PrefixWidthVecs {
4332 fn push(&mut self, separator: f32, prefix_width: f32) {
4333 self.separator_before.push(separator);
4334 self.prefix_widths.push(prefix_width);
4335 }
4336}
4337
4338#[derive(Default)]
4341struct LineWidthSink {
4342 first_separator: Option<f32>,
4343 last_width: f32,
4344 non_finite: bool,
4345}
4346
4347impl PrefixWidthSink for LineWidthSink {
4348 fn push(&mut self, separator: f32, prefix_width: f32) {
4349 self.first_separator.get_or_insert(separator);
4350 self.last_width = prefix_width;
4351 self.non_finite |= !separator.is_finite() || !prefix_width.is_finite();
4352 }
4353}
4354
4355impl LineWidthSink {
4356 fn width(&self, non_empty_overhang: f32) -> Option<f32> {
4359 let overhang = non_empty_overhang.max(0.0);
4360 if self.non_finite || !overhang.is_finite() {
4361 return None;
4362 }
4363 Some(self.first_separator.map_or(0.0, |separator| {
4364 (self.last_width - separator).max(0.0) + overhang
4365 }))
4366 }
4367}
4368
4369fn walk_line_prefix_widths(
4372 text: &AnnotatedString,
4373 line_range: std::ops::Range<usize>,
4374 style: &TextStyle,
4375 fonts: &SoftwareTextFontSet,
4376 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
4377 sink: &mut impl PrefixWidthSink,
4378) -> f32 {
4379 let mut boundaries = text.span_boundaries();
4380 boundaries.push(line_range.start);
4381 boundaries.push(line_range.end);
4382 boundaries.sort_unstable();
4383 boundaries.dedup();
4384 boundaries.retain(|offset| {
4385 *offset >= line_range.start
4386 && *offset <= line_range.end
4387 && text.text.is_char_boundary(*offset)
4388 });
4389
4390 let mut walk = PrefixWidthWalk {
4391 sink,
4392 width: 0.0,
4393 non_empty_overhang: 0.0,
4394 };
4395 for range in boundaries.windows(2) {
4396 let start = range[0];
4397 let end = range[1];
4398 if start >= end {
4399 continue;
4400 }
4401 let segment = &text.text[start..end];
4402 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
4403 append_prefix_width_segment(segment, &segment_style, fonts, glyph_metrics, &mut walk);
4404 }
4405 walk.non_empty_overhang
4406}
4407
4408struct PrefixWidthWalk<'a, S> {
4409 sink: &'a mut S,
4410 width: f32,
4411 non_empty_overhang: f32,
4412}
4413
4414impl<S: PrefixWidthSink> PrefixWidthWalk<'_, S> {
4415 fn advance(&mut self, separator: f32, step: f32) {
4417 self.width += step;
4418 self.sink.push(separator, self.width.max(0.0));
4419 }
4420}
4421
4422fn append_prefix_width_segment<S: PrefixWidthSink>(
4423 segment: &str,
4424 style: &TextStyle,
4425 fonts: &SoftwareTextFontSet,
4426 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
4427 walk: &mut PrefixWidthWalk<'_, S>,
4428) {
4429 if segment.is_empty() {
4430 return;
4431 }
4432
4433 let font_size = resolve_font_size(style);
4434 if fonts
4435 .visit_font_runs(segment, style, |range, font| {
4436 append_font_prefix_width_segment(
4437 &segment[range],
4438 style,
4439 font_size,
4440 font,
4441 glyph_metrics,
4442 walk,
4443 );
4444 })
4445 .is_none()
4446 {
4447 let char_width = fallback_char_width(font_size);
4448 let letter_spacing = resolve_letter_spacing(style, font_size);
4449 for _ in segment.chars() {
4450 walk.advance(0.0, letter_spacing + char_width);
4451 }
4452 }
4453}
4454
4455fn append_font_prefix_width_segment<S: PrefixWidthSink>(
4456 segment: &str,
4457 style: &TextStyle,
4458 font_size: f32,
4459 font: &SoftwareTextFont,
4460 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
4461 walk: &mut PrefixWidthWalk<'_, S>,
4462) {
4463 let shaped = font.shaped_for(style);
4464 let font = &*shaped;
4465 let glyph_font_size = font.ab_glyph_px_size(font_size);
4466 let scaled_font = font.font.as_scaled(PxScale::from(glyph_font_size));
4467 let letter_spacing = font.metadata.tracking.resolve(style, font_size);
4468 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, 1.0);
4469 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0);
4470 walk.non_empty_overhang = walk
4471 .non_empty_overhang
4472 .max(style_synthesis.visual_overhang_px());
4473
4474 let mut previous = None;
4475 let h_scale = scaled_font.h_scale_factor();
4476
4477 for (index, ch) in segment.chars().enumerate() {
4478 let metrics = glyph_metrics.glyph_metrics(font, &scaled_font, ch);
4479 let separator = if index == 0 {
4480 0.0
4481 } else {
4482 previous.map_or(0.0, |previous| {
4483 weight_synthesis.apply_width(
4484 glyph_metrics.kern(font, &scaled_font, previous, (ch, metrics.glyph_id))
4485 * h_scale,
4486 )
4487 })
4488 };
4489 walk.advance(
4490 separator,
4491 separator
4492 + letter_spacing
4493 + weight_synthesis.apply_width(metrics.advance_unscaled * h_scale),
4494 );
4495 previous = Some((ch, metrics.glyph_id));
4496 }
4497}
4498
4499fn byte_offset_for_char_index(text: &str, char_index: usize) -> usize {
4500 text.char_indices()
4501 .map(|(index, _)| index)
4502 .nth(char_index)
4503 .unwrap_or(text.len())
4504}
4505
4506trait LineAdvance: Font {
4508 fn line_width(&self, text: &str, font_size: f32) -> f32;
4510}
4511
4512impl LineAdvance for FontArc {
4513 fn line_width(&self, text: &str, font_size: f32) -> f32 {
4514 line_advance_width(self, text, font_size)
4515 }
4516}
4517
4518impl LineAdvance for KernedFont {
4519 fn line_width(&self, text: &str, font_size: f32) -> f32 {
4520 self.line_advance_width(text, font_size)
4521 }
4522}
4523
4524fn measure_text_impl(
4525 text: &str,
4526 style: &TextStyle,
4527 font_size: f32,
4528 font_ref: RasterFontRef<'_, impl LineAdvance>,
4529) -> TextMetrics {
4530 let font = font_ref.font;
4531 let glyph_font_size = font_size * font_ref.ab_glyph_scale_factor;
4532 let line_box = line_box_for(
4533 style,
4534 vertical_metrics(font, glyph_font_size),
4535 asked_line_height(style, 1.0),
4536 measure_grid(),
4537 );
4538 let line_height = line_box.height;
4539 let letter_spacing = font_ref.tracking.resolve(style, font_size);
4540 let weight_synthesis = TextWeightSynthesis::for_style(style, font_ref.weight, font_size, 1.0);
4541 let style_synthesis = TextStyleSynthesis::for_style(style, font_ref.style, font_size, 1.0);
4542
4543 let mut max_width: f32 = 0.0;
4544 let mut line_count = 0usize;
4545 for line in text.split('\n') {
4546 line_count += 1;
4547 let line_width = font.line_width(line, glyph_font_size);
4548 let char_spacing = run_tracking(line.chars().count(), letter_spacing);
4549 let line_width = (weight_synthesis.apply_width(line_width) + char_spacing).max(0.0);
4550 let line_width = if line.is_empty() {
4551 line_width
4552 } else {
4553 line_width + style_synthesis.visual_overhang_px()
4554 };
4555 max_width = max_width.max(line_width);
4556 }
4557
4558 TextMetrics {
4559 width: max_width,
4560 height: line_box.block_height(line_count),
4561 line_height,
4562 line_count,
4563 }
4564}
4565
4566fn measure_text_impl_cached(
4567 text: &str,
4568 style: &TextStyle,
4569 font_size: f32,
4570 font: &SoftwareTextFont,
4571 cache: &mut SoftwareTextMetricsCache,
4572) -> TextMetrics {
4573 let letter_spacing = font.metadata.tracking.resolve(style, font_size);
4574 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, 1.0);
4575 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0);
4576 let glyph_font_size = font.ab_glyph_px_size(font_size);
4577 let line_box = font_line_box(style, font, font_size);
4578 let line_height = line_box.height;
4579
4580 let mut max_width: f32 = 0.0;
4581 let mut line_count = 0usize;
4582 for line in text.split('\n') {
4583 line_count += 1;
4584 let line_width =
4585 cached_line_advance_width(font, line, glyph_font_size, &mut cache.glyph_metrics);
4586 let char_spacing = run_tracking(line.chars().count(), letter_spacing);
4587 let line_width = (weight_synthesis.apply_width(line_width) + char_spacing).max(0.0);
4588 let line_width = if line.is_empty() {
4589 line_width
4590 } else {
4591 line_width + style_synthesis.visual_overhang_px()
4592 };
4593 max_width = max_width.max(line_width);
4594 }
4595
4596 TextMetrics {
4597 width: max_width,
4598 height: line_box.block_height(line_count),
4599 line_height,
4600 line_count,
4601 }
4602}
4603
4604fn measure_annotated_text_with_resolver<'a>(
4605 text: impl Into<StyledTextRef<'a>>,
4606 style: &TextStyle,
4607 font_size: f32,
4608 fonts: impl Into<FontResolver<'a>>,
4609 cache: Option<&mut SoftwareTextMetricsCache>,
4610) -> TextMetrics {
4611 let text = text.into();
4612 let fonts = fonts.into();
4613 let Some(layout) = AnnotatedTextLayout::new(text, style, font_size, 1.0, measure_grid(), fonts)
4614 else {
4615 return fallback_text_metrics(text.text, style, font_size);
4616 };
4617 measure_resolved_text(&layout, cache)
4618}
4619
4620fn measure_resolved_text(
4621 layout: &AnnotatedTextLayout<'_>,
4622 mut cache: Option<&mut SoftwareTextMetricsCache>,
4623) -> TextMetrics {
4624 layout
4625 .walk(None, |segment| {
4626 #[cfg(feature = "text-shaping")]
4627 if requires_font_layout(segment.text, segment.style) {
4628 return Some(
4629 measure_shaped_text(
4630 segment.text,
4631 segment.style,
4632 segment.font_size,
4633 segment.font,
4634 )
4635 .width,
4636 );
4637 }
4638 let metrics = if let Some(cache) = cache.as_deref_mut() {
4639 measure_text_with_font_cached(
4640 segment.text,
4641 segment.style,
4642 segment.font_size,
4643 segment.font,
4644 cache,
4645 )
4646 } else {
4647 measure_text_with_font(segment.text, segment.style, segment.font_size, segment.font)
4648 };
4649 Some(metrics.width)
4650 })
4651 .unwrap_or_else(|| layout.metrics(0.0))
4652}
4653
4654fn max_line_height_for_annotated_text_with_resolver(
4655 text: &AnnotatedString,
4656 style: &TextStyle,
4657 font_size: f32,
4658 fonts: &SoftwareTextFontSet,
4659) -> f32 {
4660 if text.span_styles.is_empty()
4661 && let Some(font) = fonts.single_font_for_text(text.text.as_str(), style)
4662 {
4663 return font_line_box(style, font, font_size).height;
4664 }
4665 AnnotatedTextLayout::new(text.into(), style, font_size, 1.0, measure_grid(), fonts).map_or_else(
4666 || fallback_line_height(style, font_size),
4667 |layout| layout.metrics(0.0).line_height,
4668 )
4669}
4670
4671pub(crate) fn visit_annotated_line_boxes(
4672 text: &AnnotatedString,
4673 style: &TextStyle,
4674 fonts: &SoftwareTextFontSet,
4675 visit: &mut dyn FnMut(cranpose_ui::text::LineBox),
4676) -> Option<()> {
4677 let layout = AnnotatedTextLayout::new(
4678 text.into(),
4679 style,
4680 resolve_font_size(style),
4681 1.0,
4682 measure_grid(),
4683 fonts,
4684 )?;
4685 for line in &layout.lines {
4686 visit(line.line_box);
4687 }
4688 Some(())
4689}
4690
4691pub(crate) fn effective_style_for_range<'a>(
4692 span_styles: &[RangeStyle<SpanStyle>],
4693 style: &'a TextStyle,
4694 start: usize,
4695 end: usize,
4696) -> Cow<'a, TextStyle> {
4697 let mut effective = Cow::Borrowed(style);
4698 for span in span_styles {
4699 if span.range.start < end && span.range.end > start {
4700 let merged = effective.span_style.merge(&span.item);
4701 effective.to_mut().span_style = merged;
4702 }
4703 }
4704 effective
4705}
4706
4707fn clamp_to_char_boundary(text: &str, mut offset: usize) -> usize {
4708 offset = offset.min(text.len());
4709 while offset > 0 && !text.is_char_boundary(offset) {
4710 offset -= 1;
4711 }
4712 offset
4713}
4714
4715fn align_glyph_for_text_motion(glyph: Glyph, static_text_motion: bool) -> Glyph {
4716 align_glyph_to_pixel_grid(glyph, static_text_motion)
4717}
4718
4719fn static_glyph_pixel_origin(glyph: &Glyph) -> (i32, i32) {
4720 (
4721 whole::round(glyph.position.x) as i32,
4722 whole::round(glyph.position.y) as i32,
4723 )
4724}
4725
4726fn glyph_mask_cache_key(
4727 font_hash: u64,
4728 glyph: &Glyph,
4729 raster_style: GlyphRasterStyle,
4730 weight_synthesis: TextWeightSynthesis,
4731 style_synthesis: TextStyleSynthesis,
4732) -> GlyphMaskCacheKey {
4733 GlyphMaskCacheKey {
4734 font_hash,
4735 glyph_id: u32::from(glyph.id.0),
4736 scale_x_bits: glyph.scale.x.to_bits(),
4737 scale_y_bits: glyph.scale.y.to_bits(),
4738 raster_style: GlyphRasterStyleKey::from_style(raster_style),
4739 embolden_px_bits: weight_synthesis.embolden_px.to_bits(),
4740 slant_bits: style_synthesis.slant.to_bits(),
4741 }
4742}
4743
4744fn glyph_atlas_key_from_mask_key(key: GlyphMaskCacheKey) -> Option<SoftwareGlyphAtlasKey> {
4745 if !matches!(key.raster_style, GlyphRasterStyleKey::Fill) {
4746 return None;
4747 }
4748 Some(SoftwareGlyphAtlasKey {
4749 font_hash: key.font_hash,
4750 glyph_id: key.glyph_id,
4751 scale_x_bits: key.scale_x_bits,
4752 scale_y_bits: key.scale_y_bits,
4753 embolden_px_bits: key.embolden_px_bits,
4754 slant_bits: key.slant_bits,
4755 })
4756}
4757
4758fn build_complete_glyph_mask(
4759 font: &impl Font,
4760 glyph: &Glyph,
4761 raster_style: GlyphRasterStyle,
4762 weight_synthesis: TextWeightSynthesis,
4763 style_synthesis: TextStyleSynthesis,
4764 clip: Option<&GlyphRasterClip>,
4765) -> Option<GlyphMask> {
4766 let (outlined, bounds) = outline_glyph_with_bounds(font, glyph)?;
4767 if clip.is_some_and(|clip| {
4768 !clip.intersects(complete_glyph_bounds(
4769 bounds,
4770 raster_style,
4771 weight_synthesis,
4772 style_synthesis,
4773 ))
4774 }) {
4775 return None;
4776 }
4777 let mask = build_glyph_mask(font, glyph, &outlined, bounds, raster_style)?;
4778 let mask = synthesize_glyph_weight(mask, weight_synthesis);
4779 Some(synthesize_glyph_style(mask, style_synthesis).into_glyph_mask())
4780}
4781
4782fn complete_glyph_bounds(
4783 bounds: GlyphPixelBounds,
4784 raster_style: GlyphRasterStyle,
4785 weight: TextWeightSynthesis,
4786 style: TextStyleSynthesis,
4787) -> Rect {
4788 let pad = match raster_style {
4789 GlyphRasterStyle::Fill => 0.0,
4790 GlyphRasterStyle::Stroke { width_px } => stroke_mask_padding(width_px) as f32,
4791 };
4792 let horizontal = synthetic_weight_shift_px(weight.embolden_px);
4793 let vertical = (horizontal / 2).min(1) as f32;
4794 let height = bounds.height() as f32 + 2.0 * (pad + vertical);
4795 let slant = if style.slant > 0.0 {
4796 whole::ceil((height - 1.0).max(0.0) * style.slant) + 1.0
4797 } else {
4798 0.0
4799 };
4800 Rect {
4801 x: bounds.min_x as f32 - pad,
4802 y: bounds.min_y as f32 - pad - vertical,
4803 width: bounds.width() as f32 + 2.0 * pad + horizontal as f32 + slant,
4804 height,
4805 }
4806}
4807
4808#[expect(clippy::too_many_arguments)]
4809fn raster_glyph_mask(
4810 cache: Option<&mut SoftwareGlyphRasterCache>,
4811 font_hash: u64,
4812 font: &impl Font,
4813 glyph: &Glyph,
4814 raster_style: GlyphRasterStyle,
4815 weight_synthesis: TextWeightSynthesis,
4816 style_synthesis: TextStyleSynthesis,
4817 clip: Option<&GlyphRasterClip>,
4818) -> Option<GlyphMask> {
4819 if let Some(cache) = cache {
4820 let key = glyph_mask_cache_key(
4821 font_hash,
4822 glyph,
4823 raster_style,
4824 weight_synthesis,
4825 style_synthesis,
4826 );
4827 if let Some(mask) = cache.get(&key, glyph) {
4828 return clip
4829 .is_none_or(|clip| {
4830 clip.intersects(Rect {
4831 x: mask.origin_x as f32,
4832 y: mask.origin_y as f32,
4833 width: mask.width as f32,
4834 height: mask.height as f32,
4835 })
4836 })
4837 .then_some(mask);
4838 }
4839 let mask = build_complete_glyph_mask(
4840 font,
4841 glyph,
4842 raster_style,
4843 weight_synthesis,
4844 style_synthesis,
4845 clip,
4846 )?;
4847 return Some(cache.put(key, glyph, mask));
4848 }
4849 build_complete_glyph_mask(
4850 font,
4851 glyph,
4852 raster_style,
4853 weight_synthesis,
4854 style_synthesis,
4855 clip,
4856 )
4857}
4858
4859fn line_alignment_offsets<F: Font, S: ScaleFont<F>>(
4860 scaled_font: &S,
4861 text: &str,
4862 letter_spacing: f32,
4863 align_fraction: f32,
4864) -> Option<Vec<f32>> {
4865 if align_fraction == 0.0 || !text.contains('\n') {
4866 return None;
4867 }
4868 let advances: Vec<f32> = text
4869 .split('\n')
4870 .map(|line| {
4871 let mut advance = 0.0f32;
4872 let mut previous = None;
4873 for ch in line.chars() {
4874 let glyph_id = scaled_font.glyph_id(ch);
4875 if let Some(previous_id) = previous {
4876 advance += scaled_font.kern(previous_id, glyph_id);
4877 }
4878 advance += letter_spacing + scaled_font.h_advance(glyph_id);
4879 previous = Some(glyph_id);
4880 }
4881 advance.max(0.0)
4882 })
4883 .collect();
4884 let block = advances.iter().copied().fold(0.0f32, f32::max);
4885 Some(
4886 advances
4887 .iter()
4888 .map(|advance| ((block - advance) * align_fraction).max(0.0))
4889 .collect(),
4890 )
4891}
4892
4893fn line_offset(offsets: &Option<Vec<f32>>, line_idx: usize) -> f32 {
4894 offsets
4895 .as_ref()
4896 .and_then(|offsets| offsets.get(line_idx).copied())
4897 .unwrap_or(0.0)
4898}
4899
4900fn cached_kern<F: Font, S: ScaleFont<F>>(
4903 glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
4904 font_hash: u64,
4905 scaled_font: &S,
4906 previous: GlyphId,
4907 glyph: GlyphId,
4908) -> f32 {
4909 match glyph_cache {
4910 Some(cache) => {
4911 cache.kern_unscaled(font_hash, scaled_font.font(), previous, glyph)
4912 * scaled_font.h_scale_factor()
4913 }
4914 None => scaled_font.kern(previous, glyph),
4915 }
4916}
4917
4918#[expect(clippy::too_many_arguments)]
4919fn visit_text_glyph_masks(
4920 text: &str,
4921 font: &impl Font,
4922 font_hash: u64,
4923 font_px_size: f32,
4924 line_height: f32,
4925 first_baseline_y: f32,
4926 origin_x: f32,
4927 origin_y: f32,
4928 letter_spacing: f32,
4929 align_fraction: f32,
4930 static_text_motion: bool,
4931 raster_style: GlyphRasterStyle,
4932 weight_synthesis: TextWeightSynthesis,
4933 style_synthesis: TextStyleSynthesis,
4934 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
4935 clip: Option<&GlyphRasterClip>,
4936 mut visit: impl FnMut(&GlyphMask),
4937) -> f32 {
4938 let scale = PxScale::from(font_px_size);
4939 let scaled_font = font.as_scaled(scale);
4940 let line_offsets = line_alignment_offsets(&scaled_font, text, letter_spacing, align_fraction);
4941 let mut max_advance = 0.0f32;
4942 for (line_idx, line) in text.split('\n').enumerate() {
4943 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
4944 let lead_in = run_lead_in(line, letter_spacing);
4945 let mut caret_x = origin_x + line_offset(&line_offsets, line_idx) + lead_in;
4946 let mut previous = None;
4947 for ch in line.chars() {
4948 let glyph_id = scaled_font.glyph_id(ch);
4949 if let Some(previous_id) = previous {
4950 caret_x += cached_kern(
4951 glyph_cache.as_deref_mut(),
4952 font_hash,
4953 &scaled_font,
4954 previous_id,
4955 glyph_id,
4956 ) + letter_spacing;
4957 }
4958 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
4959 caret_x += scaled_font.h_advance(glyph_id);
4960 previous = Some(glyph_id);
4961 let glyph = align_glyph_for_text_motion(glyph, static_text_motion);
4962 let Some(mask) = raster_glyph_mask(
4963 if static_text_motion {
4964 glyph_cache.as_deref_mut()
4965 } else {
4966 None
4967 },
4968 font_hash,
4969 font,
4970 &glyph,
4971 raster_style,
4972 weight_synthesis,
4973 style_synthesis,
4974 clip,
4975 ) else {
4976 continue;
4977 };
4978 visit(&mask);
4979 }
4980 max_advance = max_advance.max((caret_x - origin_x + lead_in).max(0.0));
4981 }
4982 max_advance
4983}
4984
4985fn blend_src_over(dst: &mut [f32; 4], src: [f32; 4]) {
4986 let src_alpha = src[3].clamp(0.0, 1.0);
4987 if src_alpha <= 0.0 {
4988 return;
4989 }
4990
4991 let dst_alpha = dst[3].clamp(0.0, 1.0);
4992 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
4993
4994 if out_alpha <= f32::EPSILON {
4995 *dst = [0.0, 0.0, 0.0, 0.0];
4996 return;
4997 }
4998
4999 for channel in 0..3 {
5000 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
5001 let dst_premult = dst[channel].clamp(0.0, 1.0) * dst_alpha;
5002 dst[channel] =
5003 ((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0);
5004 }
5005 dst[3] = out_alpha;
5006}
5007
5008fn draw_mask_glyph(
5009 canvas: &mut [[f32; 4]],
5010 width: u32,
5011 height: u32,
5012 mask: &GlyphMask,
5013 paint: &GlyphPaint<'_>,
5014) {
5015 let correction = TextLuminance::of_brush(paint.brush).correction();
5016 for y in 0..mask.height {
5017 let py = mask.origin_y + y as i32;
5018 if py < 0 || py >= height as i32 {
5019 continue;
5020 }
5021
5022 for x in 0..mask.width {
5023 let px = mask.origin_x + x as i32;
5024 if px < 0 || px >= width as i32 {
5025 continue;
5026 }
5027
5028 let coverage = correction.apply_unit(mask.alpha[y * mask.width + x]);
5029 if coverage <= 0.0 {
5030 continue;
5031 }
5032
5033 let sample = sample_brush_rgba(
5034 paint.brush,
5035 paint.brush_rect,
5036 paint.canvas_origin.x + px as f32 + 0.5,
5037 paint.canvas_origin.y + py as f32 + 0.5,
5038 cranpose_ui_graphics::Point::default(),
5039 );
5040 let alpha = coverage * sample[3] * paint.alpha;
5041 if alpha <= 0.0 {
5042 continue;
5043 }
5044 let idx = (py as u32 * width + px as u32) as usize;
5045 blend_src_over(
5046 &mut canvas[idx],
5047 [sample[0], sample[1], sample[2], alpha.clamp(0.0, 1.0)],
5048 );
5049 }
5050 }
5051}
5052
5053fn blend_src_over_u8(dst: &mut [u8], src: [f32; 4]) {
5054 let src_alpha = src[3].clamp(0.0, 1.0);
5055 if src_alpha <= 0.0 {
5056 return;
5057 }
5058
5059 let dst_alpha = dst[3] as f32 / 255.0;
5060 if dst_alpha <= 0.0 {
5061 dst[0] = whole::round(src[0].clamp(0.0, 1.0) * 255.0) as u8;
5062 dst[1] = whole::round(src[1].clamp(0.0, 1.0) * 255.0) as u8;
5063 dst[2] = whole::round(src[2].clamp(0.0, 1.0) * 255.0) as u8;
5064 dst[3] = whole::round(src_alpha * 255.0) as u8;
5065 return;
5066 }
5067
5068 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
5069 if out_alpha <= f32::EPSILON {
5070 dst.fill(0);
5071 return;
5072 }
5073
5074 for channel in 0..3 {
5075 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
5076 let dst_premult = (dst[channel] as f32 / 255.0) * dst_alpha;
5077 dst[channel] =
5078 whole::round((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha * 255.0) as u8;
5079 }
5080 dst[3] = whole::round(out_alpha.clamp(0.0, 1.0) * 255.0) as u8;
5081}
5082
5083fn draw_mask_glyph_solid_u8(
5084 canvas: &mut [u8],
5085 width: u32,
5086 height: u32,
5087 mask: &GlyphMask,
5088 color: [f32; 4],
5089 alpha_multiplier: f32,
5090) {
5091 let red = whole::round(color[0].clamp(0.0, 1.0) * 255.0) as u8;
5092 let green = whole::round(color[1].clamp(0.0, 1.0) * 255.0) as u8;
5093 let blue = whole::round(color[2].clamp(0.0, 1.0) * 255.0) as u8;
5094 let alpha_scale = color[3].clamp(0.0, 1.0) * alpha_multiplier.clamp(0.0, 1.0);
5095 if alpha_scale <= 0.0 || width == 0 || mask.width == 0 {
5096 return;
5097 }
5098 let correction = TextLuminance::of_color(Color(color[0], color[1], color[2], 1.0)).correction();
5099 let columns = canvas_span(mask.origin_x, mask.width, width);
5101 let rows = canvas_span(mask.origin_y, mask.height, height);
5102 let canvas_rows = canvas
5103 .as_chunks_mut::<4>()
5104 .0
5105 .chunks_exact_mut(width as usize);
5106 for (row, mask_row) in canvas_rows
5107 .skip(rows.start.saturating_add_signed(mask.origin_y as isize))
5108 .zip(
5109 mask.alpha
5110 .chunks_exact(mask.width)
5111 .take(rows.end)
5112 .skip(rows.start),
5113 )
5114 {
5115 let first = columns.start.saturating_add_signed(mask.origin_x as isize);
5116 let pixels = row.iter_mut().skip(first);
5117 for (dst, &mask_coverage) in pixels.zip(&mask_row[columns.clone()]) {
5118 let corrected = correction.apply(mask_coverage);
5119 if corrected == 0 {
5120 continue;
5121 }
5122 let alpha =
5125 || (crate::text_mask_gamma::unit_coverage(corrected) * alpha_scale).clamp(0.0, 1.0);
5126 let alpha_u8 = if alpha_scale == 1.0 {
5127 corrected
5128 } else {
5129 whole::round(alpha() * 255.0) as u8
5130 };
5131 if alpha_u8 == 0 {
5132 continue;
5133 }
5134 if dst[3] == 0 {
5135 *dst = [red, green, blue, alpha_u8];
5136 } else {
5137 blend_src_over_u8(dst, [color[0], color[1], color[2], alpha()]);
5138 }
5139 }
5140 }
5141}
5142
5143fn canvas_span(origin: i32, len: usize, extent: u32) -> std::ops::Range<usize> {
5146 let start = usize::try_from(-i64::from(origin)).unwrap_or(0).min(len);
5147 let end = usize::try_from(i64::from(extent) - i64::from(origin))
5148 .unwrap_or(0)
5149 .clamp(start, len);
5150 start..end
5151}
5152
5153fn draw_shadow_mask(
5154 canvas: &mut [[f32; 4]],
5155 width: u32,
5156 height: u32,
5157 mask: &GlyphMask,
5158 shadow: Shadow,
5159 text_scale: f32,
5160 static_text_motion: bool,
5161) {
5162 if mask.width == 0 || mask.height == 0 {
5163 return;
5164 }
5165
5166 let shadow_dx = shadow.offset.x * text_scale;
5167 let shadow_dy = shadow.offset.y * text_scale;
5168 let blur_radius = (shadow.blur_radius * text_scale).max(0.0);
5169 let sigma = shadow_blur_sigma(blur_radius);
5170 let blur_margin = if sigma > 0.0 {
5171 whole::ceil(sigma * 3.0) as i32
5172 } else {
5173 0
5174 };
5175
5176 let padded_width = mask.width + (blur_margin as usize) * 2;
5177 let padded_height = mask.height + (blur_margin as usize) * 2;
5178 let mut padded_mask = vec![0.0f32; padded_width * padded_height];
5179
5180 let correction = (sigma <= 0.0).then(|| TextLuminance::of_color(shadow.color).correction());
5183 for y in 0..mask.height {
5184 let src_offset = y * mask.width;
5185 let dst_offset = (y + blur_margin as usize) * padded_width + blur_margin as usize;
5186 let source = &mask.alpha[src_offset..src_offset + mask.width];
5187 let target = &mut padded_mask[dst_offset..dst_offset + mask.width];
5188 match correction {
5189 Some(correction) => target
5190 .iter_mut()
5191 .zip(source)
5192 .for_each(|(target, &coverage)| *target = correction.apply_unit(coverage)),
5193 None => target
5194 .iter_mut()
5195 .zip(source)
5196 .for_each(|(target, &coverage)| {
5197 *target = crate::text_mask_gamma::unit_coverage(coverage);
5198 }),
5199 }
5200 }
5201
5202 let blurred = if sigma > 0.0 {
5203 gaussian_blur_alpha(&padded_mask, padded_width, padded_height, sigma)
5204 } else {
5205 padded_mask
5206 };
5207
5208 let shadow_rgba = color_to_rgba(shadow.color);
5209 let shadow_origin_x = mask.origin_x - blur_margin;
5210 let shadow_origin_y = mask.origin_y - blur_margin;
5211
5212 for y in 0..padded_height {
5213 for x in 0..padded_width {
5214 let alpha = blurred[y * padded_width + x] * shadow_rgba[3];
5215 if alpha <= 0.0 {
5216 continue;
5217 }
5218
5219 let target_x = shadow_origin_x as f32 + x as f32 + shadow_dx;
5220 let target_y = shadow_origin_y as f32 + y as f32 + shadow_dy;
5221 if static_text_motion {
5222 blend_shadow_pixel(
5223 canvas,
5224 width,
5225 height,
5226 whole::round(target_x) as i32,
5227 whole::round(target_y) as i32,
5228 shadow_rgba,
5229 alpha.clamp(0.0, 1.0),
5230 );
5231 } else {
5232 blend_shadow_pixel_subpixel(
5233 canvas,
5234 width,
5235 height,
5236 target_x,
5237 target_y,
5238 shadow_rgba,
5239 alpha.clamp(0.0, 1.0),
5240 );
5241 }
5242 }
5243 }
5244}
5245
5246fn blend_shadow_pixel(
5247 canvas: &mut [[f32; 4]],
5248 width: u32,
5249 height: u32,
5250 px: i32,
5251 py: i32,
5252 color: [f32; 4],
5253 alpha: f32,
5254) {
5255 if px < 0 || py < 0 || px >= width as i32 || py >= height as i32 || alpha <= 0.0 {
5256 return;
5257 }
5258 let idx = (py as u32 * width + px as u32) as usize;
5259 blend_src_over(
5260 &mut canvas[idx],
5261 [color[0], color[1], color[2], alpha.clamp(0.0, 1.0)],
5262 );
5263}
5264
5265fn blend_shadow_pixel_subpixel(
5266 canvas: &mut [[f32; 4]],
5267 width: u32,
5268 height: u32,
5269 x: f32,
5270 y: f32,
5271 color: [f32; 4],
5272 alpha: f32,
5273) {
5274 if alpha <= 0.0 {
5275 return;
5276 }
5277
5278 let base_x = whole::floor(x);
5279 let base_y = whole::floor(y);
5280 let frac_x = x - base_x;
5281 let frac_y = y - base_y;
5282 let base_x_i32 = base_x as i32;
5283 let base_y_i32 = base_y as i32;
5284 let weights = [
5285 ((1.0 - frac_x) * (1.0 - frac_y), 0i32, 0i32),
5286 (frac_x * (1.0 - frac_y), 1, 0),
5287 ((1.0 - frac_x) * frac_y, 0, 1),
5288 (frac_x * frac_y, 1, 1),
5289 ];
5290
5291 for (weight, dx, dy) in weights {
5292 if weight <= 0.0 {
5293 continue;
5294 }
5295 blend_shadow_pixel(
5296 canvas,
5297 width,
5298 height,
5299 base_x_i32 + dx,
5300 base_y_i32 + dy,
5301 color,
5302 alpha * weight,
5303 );
5304 }
5305}
5306
5307fn shadow_blur_sigma(blur_radius: f32) -> f32 {
5308 if blur_radius <= 0.0 {
5309 0.0
5310 } else {
5311 (blur_radius * SHADOW_SIGMA_SCALE + SHADOW_SIGMA_BIAS).max(0.5)
5312 }
5313}
5314
5315fn gaussian_blur_alpha(src: &[f32], width: usize, height: usize, sigma: f32) -> Vec<f32> {
5316 let kernel = gaussian_kernel_1d(sigma);
5317 if kernel.len() == 1 {
5318 return src.to_vec();
5319 }
5320 let half = (kernel.len() / 2) as i32;
5321
5322 let mut horizontal = vec![0.0f32; src.len()];
5323 for y in 0..height {
5324 for x in 0..width {
5325 let mut sum = 0.0f32;
5326 for (index, weight) in kernel.iter().enumerate() {
5327 let offset = index as i32 - half;
5328 let sample_x = (x as i32 + offset).clamp(0, width as i32 - 1) as usize;
5329 sum += src[y * width + sample_x] * *weight;
5330 }
5331 horizontal[y * width + x] = sum;
5332 }
5333 }
5334
5335 let mut output = vec![0.0f32; src.len()];
5336 for y in 0..height {
5337 for x in 0..width {
5338 let mut sum = 0.0f32;
5339 for (index, weight) in kernel.iter().enumerate() {
5340 let offset = index as i32 - half;
5341 let sample_y = (y as i32 + offset).clamp(0, height as i32 - 1) as usize;
5342 sum += horizontal[sample_y * width + x] * *weight;
5343 }
5344 output[y * width + x] = sum;
5345 }
5346 }
5347
5348 output
5349}
5350
5351fn gaussian_kernel_1d(sigma: f32) -> Vec<f32> {
5352 let half = (whole::ceil(sigma * 3.0) as i32).clamp(1, MAX_GAUSSIAN_KERNEL_HALF);
5353 if half <= 0 {
5354 return vec![1.0];
5355 }
5356
5357 let mut kernel = Vec::with_capacity((half * 2 + 1) as usize);
5358 let mut sum = 0.0f32;
5359 for offset in -half..=half {
5360 let distance = offset as f32;
5361 let weight = (-0.5 * (distance / sigma).powi(2)).exp();
5362 kernel.push(weight);
5363 sum += weight;
5364 }
5365
5366 if sum > f32::EPSILON {
5367 for weight in &mut kernel {
5368 *weight /= sum;
5369 }
5370 }
5371
5372 kernel
5373}
5374
5375fn outline_glyph_with_bounds(
5376 font: &impl Font,
5377 glyph: &Glyph,
5378) -> Option<(OutlinedGlyph, GlyphPixelBounds)> {
5379 let outlined = font.outline_glyph(glyph.clone())?;
5380 let bounds = pixel_bounds_from_outlined(&outlined);
5381 Some((outlined, bounds))
5382}
5383
5384fn build_glyph_mask(
5385 font: &impl Font,
5386 glyph: &Glyph,
5387 outlined: &OutlinedGlyph,
5388 bounds: GlyphPixelBounds,
5389 style: GlyphRasterStyle,
5390) -> Option<RasterMask> {
5391 match style {
5392 GlyphRasterStyle::Fill => build_fill_mask(outlined, bounds),
5393 GlyphRasterStyle::Stroke { width_px } => {
5394 build_stroke_mask(font, glyph, outlined, bounds, width_px)
5395 }
5396 }
5397}
5398
5399fn build_fill_mask(outlined: &OutlinedGlyph, bounds: GlyphPixelBounds) -> Option<RasterMask> {
5400 let mask_width = bounds.width();
5401 let mask_height = bounds.height();
5402 if mask_width == 0 || mask_height == 0 {
5403 return None;
5404 }
5405
5406 let mut alpha = vec![0.0f32; mask_width * mask_height];
5407 outlined.draw(|gx, gy, value| {
5408 let idx = gy as usize * mask_width + gx as usize;
5409 alpha[idx] = value;
5410 });
5411
5412 Some(RasterMask {
5413 alpha,
5414 width: mask_width,
5415 height: mask_height,
5416 origin_x: bounds.min_x,
5417 origin_y: bounds.min_y,
5418 })
5419}
5420
5421#[inline(never)]
5422fn build_stroke_mask(
5423 font: &impl Font,
5424 glyph: &Glyph,
5425 outlined: &OutlinedGlyph,
5426 bounds: GlyphPixelBounds,
5427 stroke_width_px: f32,
5428) -> Option<RasterMask> {
5429 if !stroke_width_px.is_finite() || stroke_width_px <= 0.0 {
5430 return build_fill_mask(outlined, bounds);
5431 }
5432
5433 let mask_width = bounds.max_x - bounds.min_x;
5434 let mask_height = bounds.max_y - bounds.min_y;
5435 if mask_width <= 0 || mask_height <= 0 {
5436 return None;
5437 }
5438
5439 let pad = stroke_mask_padding(stroke_width_px);
5440 let path = build_outline_path(font, glyph, bounds, pad)?;
5441 let raster_width = mask_width + pad * 2;
5442 let raster_height = mask_height + pad * 2;
5443 if raster_width <= 0 || raster_height <= 0 {
5444 return None;
5445 }
5446
5447 let mut pixmap = Pixmap::new(raster_width as u32, raster_height as u32)?;
5448 let mut paint = Paint::default();
5449 paint.set_color_rgba8(255, 255, 255, 255);
5450 paint.anti_alias = true;
5451
5452 let stroke = Stroke {
5453 width: stroke_width_px,
5454 line_cap: LineCap::Butt,
5455 line_join: LineJoin::Miter,
5456 miter_limit: COMPOSE_STROKE_MITER_LIMIT,
5457 ..Stroke::default()
5458 };
5459
5460 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
5461
5462 let alpha: Vec<f32> = pixmap
5463 .data()
5464 .as_chunks::<4>()
5465 .0
5466 .iter()
5467 .map(|pixel| pixel[3] as f32 / 255.0)
5468 .collect();
5469
5470 Some(RasterMask {
5471 alpha,
5472 width: raster_width as usize,
5473 height: raster_height as usize,
5474 origin_x: bounds.min_x - pad,
5475 origin_y: bounds.min_y - pad,
5476 })
5477}
5478
5479fn stroke_mask_padding(width: f32) -> i32 {
5480 if width.is_finite() && width > 0.0 {
5481 whole::ceil(width * 0.5 * COMPOSE_STROKE_MITER_LIMIT).max(1.0) as i32 + 1
5482 } else {
5483 0
5484 }
5485}
5486
5487fn synthesize_glyph_weight(mask: RasterMask, synthesis: TextWeightSynthesis) -> RasterMask {
5488 let horizontal_shift = synthetic_weight_shift_px(synthesis.embolden_px);
5489 if horizontal_shift == 0 || mask.width == 0 || mask.height == 0 {
5490 return mask;
5491 }
5492
5493 let vertical_shift = (horizontal_shift / 2).min(1);
5494 let output_width = mask.width + horizontal_shift;
5495 let output_height = mask.height + vertical_shift * 2;
5496 let mut alpha = vec![0.0f32; output_width * output_height];
5497 for y in 0..mask.height {
5498 for x in 0..mask.width {
5499 let coverage = mask.alpha[y * mask.width + x];
5500 if coverage <= 0.0 {
5501 continue;
5502 }
5503 for dy in 0..=(vertical_shift * 2) {
5504 let output_y = y + dy;
5505 for dx in 0..=horizontal_shift {
5506 let output_x = x + dx;
5507 let output_index = output_y * output_width + output_x;
5508 if coverage > alpha[output_index] {
5509 alpha[output_index] = coverage;
5510 }
5511 }
5512 }
5513 }
5514 }
5515
5516 RasterMask {
5517 alpha,
5518 width: output_width,
5519 height: output_height,
5520 origin_x: mask.origin_x,
5521 origin_y: mask.origin_y - vertical_shift as i32,
5522 }
5523}
5524
5525fn synthesize_glyph_style(mask: RasterMask, synthesis: TextStyleSynthesis) -> RasterMask {
5526 if synthesis.slant <= 0.0 || mask.width == 0 || mask.height == 0 {
5527 return mask;
5528 }
5529
5530 let max_shift = whole::ceil((mask.height.saturating_sub(1)) as f32 * synthesis.slant) as usize;
5531 if max_shift == 0 {
5532 return mask;
5533 }
5534
5535 let output_width = mask.width + max_shift + 1;
5536 let mut alpha = vec![0.0f32; output_width * mask.height];
5537 for y in 0..mask.height {
5538 let shift = (mask.height.saturating_sub(1) - y) as f32 * synthesis.slant;
5539 let shift_floor = whole::floor(shift) as usize;
5540 let shift_fraction = shift - whole::floor(shift);
5541 for x in 0..mask.width {
5542 let coverage = mask.alpha[y * mask.width + x];
5543 if coverage <= 0.0 {
5544 continue;
5545 }
5546
5547 let output_x = x + shift_floor;
5548 let left_index = y * output_width + output_x;
5549 let left_coverage = coverage * (1.0 - shift_fraction);
5550 if left_coverage > alpha[left_index] {
5551 alpha[left_index] = left_coverage;
5552 }
5553
5554 if shift_fraction > 0.0 {
5555 let right_index = left_index + 1;
5556 let right_coverage = coverage * shift_fraction;
5557 if right_coverage > alpha[right_index] {
5558 alpha[right_index] = right_coverage;
5559 }
5560 }
5561 }
5562 }
5563
5564 RasterMask {
5565 alpha,
5566 width: output_width,
5567 height: mask.height,
5568 origin_x: mask.origin_x,
5569 origin_y: mask.origin_y,
5570 }
5571}
5572
5573fn synthetic_weight_shift_px(embolden_px: f32) -> usize {
5574 if !embolden_px.is_finite() || embolden_px < 0.35 {
5575 return 0;
5576 }
5577 whole::ceil(embolden_px).max(1.0) as usize
5578}
5579
5580fn build_outline_path(
5581 font: &impl Font,
5582 glyph: &Glyph,
5583 bounds: GlyphPixelBounds,
5584 pad: i32,
5585) -> Option<Path> {
5586 let outline = font.outline(glyph.id)?;
5587 let scale_factor = font.as_scaled(glyph.scale).scale_factor();
5588 let mut builder = PathBuilder::new();
5589 let mut has_segments = false;
5590 let mut current_end = None;
5591 let mut subpath_start = None;
5592
5593 for curve in outline.curves {
5594 match curve {
5595 ab_glyph::OutlineCurve::Line(p0, p1) => {
5596 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
5597 let end = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
5598 if current_end != Some(start) {
5599 if current_end.is_some() {
5600 builder.close();
5601 }
5602 builder.move_to(start.0, start.1);
5603 subpath_start = Some(start);
5604 }
5605 builder.line_to(end.0, end.1);
5606 if subpath_start == Some(end) {
5607 builder.close();
5608 current_end = None;
5609 subpath_start = None;
5610 } else {
5611 current_end = Some(end);
5612 }
5613 }
5614 ab_glyph::OutlineCurve::Quad(p0, p1, p2) => {
5615 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
5616 let control = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
5617 let end = transform_outline_point(p2, scale_factor, glyph, bounds, pad);
5618 if current_end != Some(start) {
5619 if current_end.is_some() {
5620 builder.close();
5621 }
5622 builder.move_to(start.0, start.1);
5623 subpath_start = Some(start);
5624 }
5625 builder.quad_to(control.0, control.1, end.0, end.1);
5626 if subpath_start == Some(end) {
5627 builder.close();
5628 current_end = None;
5629 subpath_start = None;
5630 } else {
5631 current_end = Some(end);
5632 }
5633 }
5634 ab_glyph::OutlineCurve::Cubic(p0, p1, p2, p3) => {
5635 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
5636 let control1 = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
5637 let control2 = transform_outline_point(p2, scale_factor, glyph, bounds, pad);
5638 let end = transform_outline_point(p3, scale_factor, glyph, bounds, pad);
5639 if current_end != Some(start) {
5640 if current_end.is_some() {
5641 builder.close();
5642 }
5643 builder.move_to(start.0, start.1);
5644 subpath_start = Some(start);
5645 }
5646 builder.cubic_to(control1.0, control1.1, control2.0, control2.1, end.0, end.1);
5647 if subpath_start == Some(end) {
5648 builder.close();
5649 current_end = None;
5650 subpath_start = None;
5651 } else {
5652 current_end = Some(end);
5653 }
5654 }
5655 }
5656 has_segments = true;
5657 }
5658
5659 if !has_segments {
5660 return None;
5661 }
5662
5663 if current_end.is_some() {
5664 builder.close();
5665 }
5666
5667 builder.finish()
5668}
5669
5670fn transform_outline_point(
5671 point: ab_glyph::Point,
5672 scale_factor: ab_glyph::PxScaleFactor,
5673 glyph: &Glyph,
5674 bounds: GlyphPixelBounds,
5675 pad: i32,
5676) -> (f32, f32) {
5677 (
5678 point.x * scale_factor.horizontal + glyph.position.x - bounds.min_x as f32 + pad as f32,
5679 point.y * -scale_factor.vertical + glyph.position.y - bounds.min_y as f32 + pad as f32,
5680 )
5681}
5682
5683#[cfg(test)]
5684#[path = "tests/software_text_raster_tests.rs"]
5685mod tests;
5686
5687#[cfg(test)]
5688#[path = "tests/software_text_raster_line_alignment_tests.rs"]
5689mod line_alignment_tests;
5690
5691#[cfg(test)]
5692#[path = "tests/software_text_features_tests.rs"]
5693mod feature_tests;