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