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