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