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