1use ab_glyph::{
2 point, Font, FontArc, FontVec, Glyph, GlyphId, OutlinedGlyph, PxScale, ScaleFont, VariableFont,
3};
4use cranpose_core::hash::default as default_hash;
5use cranpose_ui::text::{
6 AnnotatedString, FontFamily, FontStyle, FontSynthesis, FontWeight, RangeStyle, RenderString,
7 Shadow, SpanStyle, TextDrawStyle, TextMotion, TextShaping, TextStyle,
8};
9use cranpose_ui::text_layout_result::{GlyphLayout, LineLayout, TextLayoutData, TextLayoutResult};
10use cranpose_ui::{TextLinePrefixWidths, TextMeasurer, TextMetrics};
11use cranpose_ui_graphics::{Color, ImageBitmap, Rect};
12use std::hash::{Hash, Hasher};
13use std::rc::Rc;
14use std::sync::{Arc, Mutex, MutexGuard};
15use tiny_skia::{LineCap, LineJoin, Paint, Path, PathBuilder, Pixmap, Stroke, Transform};
16
17use crate::bounded_lru_cache::BoundedLruCache;
18use crate::brush_sampling::{color_to_rgba, sample_brush_rgba};
19#[cfg(test)]
20use crate::font_layout::layout_line_glyphs;
21use crate::font_layout::{
22 align_glyph_to_pixel_grid, line_advance_width, pixel_bounds_from_outlined, vertical_metrics,
23 GlyphPixelBounds,
24};
25#[cfg(feature = "text-hyphenation")]
26use crate::text_hyphenation::HyphenationDictionaryError;
27use crate::text_hyphenation::HyphenationDictionaryStore;
28use crate::Brush;
29
30const COMPOSE_STROKE_MITER_LIMIT: f32 = 4.0;
31const SHADOW_SIGMA_SCALE: f32 = 0.57735;
32const SHADOW_SIGMA_BIAS: f32 = 0.5;
33const MAX_GAUSSIAN_KERNEL_HALF: i32 = 128;
34const SOFTWARE_TEXT_GLYPH_METRICS_CACHE_CAPACITY: usize = 8_192;
35const SOFTWARE_TEXT_KERN_METRICS_CACHE_CAPACITY: usize = 16_384;
36const SOFTWARE_TEXT_PREFIX_WIDTH_CACHE_CAPACITY: usize = 512;
37#[cfg(feature = "embedded-default-font")]
38#[doc(hidden)]
39pub const DEFAULT_SOFTWARE_TEXT_FONT_BYTES: &[u8] = include_bytes!("../assets/NotoSansMerged.ttf");
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
42pub enum SoftwareTextFontError {
43 #[error("invalid software text font bytes")]
44 InvalidFont,
45 #[error("embedded default font disabled (feature `embedded-default-font` is off)")]
46 EmbeddedFontDisabled,
47}
48
49#[derive(Clone)]
50pub struct SoftwareTextFont {
51 font: FontArc,
52 metadata: SoftwareTextFontMetadata,
53 score: TextFontScore,
54 content_hash: u64,
55}
56
57#[derive(Clone)]
58struct SoftwareTextFontMetadata {
59 families: Arc<[String]>,
60 registered_family: Option<FontFamilyKey>,
61 weight: FontWeight,
62 style: FontStyle,
63 ab_glyph_scale_factor: f32,
64}
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74pub struct FontFamilyKey(u64);
75
76impl FontFamilyKey {
77 pub fn of(family: &FontFamily) -> Self {
78 let mut state = default_hash::new();
79 family.hash(&mut state);
80 Self(state.finish())
81 }
82}
83
84impl SoftwareTextFont {
85 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Result<Self, SoftwareTextFontError> {
86 let bytes = bytes.into();
87 let mut hasher = default_hash::new();
88 bytes.hash(&mut hasher);
89 let content_hash = hasher.finish();
90 let metadata = software_text_font_metadata(bytes.as_slice());
91 let font = FontArc::try_from_vec(bytes).map_err(|_| SoftwareTextFontError::InvalidFont)?;
92 let score =
93 text_font_score_from_parts(&font, metadata.ab_glyph_scale_factor, metadata.weight);
94 Ok(Self {
95 font,
96 metadata,
97 score,
98 content_hash,
99 })
100 }
101
102 pub fn from_registered_bytes(
112 family: &FontFamily,
113 weight: FontWeight,
114 style: FontStyle,
115 bytes: impl Into<Vec<u8>>,
116 ) -> Result<Self, SoftwareTextFontError> {
117 let bytes = bytes.into();
118 let mut hasher = default_hash::new();
119 bytes.hash(&mut hasher);
120 let mut metadata = software_text_font_metadata(bytes.as_slice());
121 metadata.registered_family = Some(FontFamilyKey::of(family));
122 metadata.weight = weight;
123 metadata.style = style;
124
125 let mut font =
126 FontVec::try_from_vec(bytes).map_err(|_| SoftwareTextFontError::InvalidFont)?;
127 for (tag, value) in apply_declared_variations(&mut font, weight, style) {
130 tag.hash(&mut hasher);
131 value.to_bits().hash(&mut hasher);
132 }
133 let content_hash = hasher.finish();
134
135 let font = FontArc::from(font);
136 let score = text_font_score_from_parts(&font, metadata.ab_glyph_scale_factor, weight);
137 Ok(Self {
138 font,
139 metadata,
140 score,
141 content_hash,
142 })
143 }
144
145 pub fn family_names(&self) -> &[String] {
146 &self.metadata.families
147 }
148
149 pub fn registered_family(&self) -> Option<FontFamilyKey> {
151 self.metadata.registered_family
152 }
153
154 pub fn weight(&self) -> FontWeight {
155 self.metadata.weight
156 }
157
158 pub fn style(&self) -> FontStyle {
159 self.metadata.style
160 }
161
162 fn ab_glyph_px_size(&self, logical_font_size: f32) -> f32 {
163 logical_font_size * self.metadata.ab_glyph_scale_factor
164 }
165
166 pub fn content_hash(&self) -> u64 {
169 self.content_hash
170 }
171}
172
173pub fn try_default_software_text_font() -> Result<SoftwareTextFont, SoftwareTextFontError> {
174 #[cfg(feature = "embedded-default-font")]
175 {
176 SoftwareTextFont::from_bytes(DEFAULT_SOFTWARE_TEXT_FONT_BYTES.to_vec())
177 }
178 #[cfg(not(feature = "embedded-default-font"))]
179 {
180 Err(SoftwareTextFontError::EmbeddedFontDisabled)
181 }
182}
183
184pub fn default_software_text_font() -> Option<SoftwareTextFont> {
185 try_default_software_text_font().ok()
186}
187
188#[derive(Clone)]
189pub struct SoftwareTextFontSet {
190 fonts: Arc<[SoftwareTextFont]>,
191 registered_families: Arc<[FontFamilyKey]>,
192 default_index: Option<usize>,
193}
194
195impl SoftwareTextFontSet {
196 pub fn empty() -> Self {
197 Self::from_faces(Vec::new())
198 }
199
200 pub fn from_font(font: SoftwareTextFont) -> Self {
201 Self::from_faces(vec![font])
202 }
203
204 pub fn from_faces(fonts: Vec<SoftwareTextFont>) -> Self {
207 let mut registered_families: Vec<FontFamilyKey> = Vec::new();
208 for family in fonts.iter().filter_map(SoftwareTextFont::registered_family) {
209 if !registered_families.contains(&family) {
210 registered_families.push(family);
211 }
212 }
213 let default_index = (!fonts.is_empty()).then(|| default_font_index(&fonts));
214 Self {
215 fonts: Arc::from(fonts),
216 registered_families: Arc::from(registered_families),
217 default_index,
218 }
219 }
220
221 pub fn from_fonts_or_default(fonts: &[&[u8]]) -> Self {
222 let mut parsed = Vec::with_capacity(fonts.len().max(1));
223 for font in fonts {
224 if let Ok(candidate) = SoftwareTextFont::from_bytes((*font).to_vec()) {
225 parsed.push(candidate);
226 }
227 }
228 if parsed.is_empty() {
229 if let Some(default_font) = default_software_text_font() {
230 parsed.push(default_font);
231 }
232 }
233
234 Self::from_faces(parsed)
235 }
236
237 pub fn default_font(&self) -> Option<&SoftwareTextFont> {
238 self.default_index.and_then(|index| self.fonts.get(index))
239 }
240
241 pub fn faces(&self) -> &[SoftwareTextFont] {
243 &self.fonts
244 }
245
246 pub fn has_registered_family(&self, family: &FontFamily) -> bool {
248 self.registered_families
249 .contains(&FontFamilyKey::of(family))
250 }
251
252 pub fn resolve(&self, style: &TextStyle) -> Option<&SoftwareTextFont> {
253 let target_weight = style.span_style.font_weight.unwrap_or_default();
254 let target_style = style.span_style.font_style.unwrap_or_default();
255 let request = FontFamilyRequest::resolve(
256 style.span_style.font_family.as_ref(),
257 &self.registered_families,
258 );
259
260 let mut best: Option<(usize, u32)> = None;
261 for (index, font) in self.fonts.iter().enumerate() {
262 let Some(score) = font_match_score(font, target_weight, target_style, request) else {
263 continue;
264 };
265 if best.is_none_or(|(_, best_score)| score < best_score) {
266 best = Some((index, score));
267 }
268 }
269
270 let index = best.map(|(index, _)| index).or(self.default_index);
271 index.and_then(|index| self.fonts.get(index))
272 }
273}
274
275pub fn software_text_font_from_fonts_or_default(fonts: &[&[u8]]) -> Option<SoftwareTextFont> {
276 SoftwareTextFontSet::from_fonts_or_default(fonts)
277 .default_font()
278 .cloned()
279}
280
281pub fn software_text_font_set_from_fonts_or_default(fonts: &[&[u8]]) -> SoftwareTextFontSet {
282 SoftwareTextFontSet::from_fonts_or_default(fonts)
283}
284
285#[derive(Clone, Copy)]
286struct TextFontScore {
287 supported_latin_chars: usize,
288 latin_sample_width: f32,
289}
290
291impl TextFontScore {
292 fn is_complete_default_face(self) -> bool {
293 const LATIN_SAMPLE_CHAR_COUNT: usize = 21;
294 self.supported_latin_chars == LATIN_SAMPLE_CHAR_COUNT && self.latin_sample_width > 1.0
295 }
296
297 fn is_better_than(self, other: Self) -> bool {
298 self.supported_latin_chars > other.supported_latin_chars
299 || (self.supported_latin_chars == other.supported_latin_chars
300 && self.latin_sample_width > other.latin_sample_width)
301 }
302}
303
304fn text_font_score(font: &SoftwareTextFont) -> TextFontScore {
305 font.score
306}
307
308fn text_font_score_from_parts(
309 font: &FontArc,
310 ab_glyph_scale_factor: f32,
311 weight: FontWeight,
312) -> TextFontScore {
313 const SAMPLE: &str = "UNDER The quick brown fox";
314 let glyph_font_size = 18.0 * ab_glyph_scale_factor;
315 let scaled_font = font.as_scaled(PxScale::from(glyph_font_size));
316 let supported_latin_chars = SAMPLE
317 .chars()
318 .filter(|ch| !ch.is_whitespace())
319 .filter(|ch| scaled_font.glyph_id(*ch).0 != 0)
320 .count();
321 let latin_sample_width = measure_text_impl(
322 SAMPLE,
323 &TextStyle::default(),
324 18.0,
325 glyph_font_size,
326 font,
327 FontStyle::Normal,
328 weight,
329 )
330 .width;
331 TextFontScore {
332 supported_latin_chars,
333 latin_sample_width,
334 }
335}
336
337fn default_font_index(fonts: &[SoftwareTextFont]) -> usize {
338 let mut best: Option<(usize, TextFontScore)> = None;
339 for (index, font) in fonts.iter().enumerate() {
340 let score = text_font_score(font);
341 if font.style() == FontStyle::Normal
342 && font.weight() == FontWeight::NORMAL
343 && score.is_complete_default_face()
344 {
345 return index;
346 }
347 if best
348 .as_ref()
349 .is_none_or(|(_, best_score)| score.is_better_than(*best_score))
350 {
351 best = Some((index, score));
352 }
353 }
354 best.map(|(index, _)| index).unwrap_or(0)
355}
356
357#[derive(Clone, Copy)]
362enum FontFamilyRequest<'a> {
363 Any,
367 Named { name: &'a str, key: FontFamilyKey },
370 Registered(FontFamilyKey),
372}
373
374impl<'a> FontFamilyRequest<'a> {
375 fn resolve(font_family: Option<&'a FontFamily>, registered: &[FontFamilyKey]) -> Self {
376 match font_family {
377 None | Some(FontFamily::Default) => Self::Any,
378 Some(FontFamily::Named(name)) => Self::Named {
379 name: name.as_str(),
380 key: FontFamilyKey::of(&FontFamily::Named(name.clone())),
381 },
382 Some(family @ (FontFamily::FileBacked(_) | FontFamily::LoadedTypeface(_))) => {
383 Self::Registered(FontFamilyKey::of(family))
384 }
385 Some(family) => {
386 let key = FontFamilyKey::of(family);
390 if registered.contains(&key) {
391 Self::Registered(key)
392 } else {
393 Self::Any
394 }
395 }
396 }
397 }
398
399 fn matches(self, font: &SoftwareTextFont) -> bool {
400 match self {
401 Self::Any => true,
402 Self::Named { name, key } => {
403 font_family_matches(font, name) || font.registered_family() == Some(key)
404 }
405 Self::Registered(key) => font.registered_family() == Some(key),
406 }
407 }
408}
409
410fn font_match_score(
411 font: &SoftwareTextFont,
412 target_weight: FontWeight,
413 target_style: FontStyle,
414 request: FontFamilyRequest<'_>,
415) -> Option<u32> {
416 if !request.matches(font) {
417 return None;
418 }
419 let style_penalty = if font.style() == target_style {
420 0
421 } else {
422 10_000
423 };
424 let weight_penalty = (i32::from(font.weight().0) - i32::from(target_weight.0)).unsigned_abs();
425 let coverage_penalty =
426 (21usize.saturating_sub(text_font_score(font).supported_latin_chars) as u32) * 1_000;
427
428 Some(style_penalty + weight_penalty + coverage_penalty)
429}
430
431fn font_family_matches(font: &SoftwareTextFont, requested: &str) -> bool {
432 font.family_names()
433 .iter()
434 .any(|family| family.eq_ignore_ascii_case(requested))
435}
436
437fn apply_declared_variations(
443 font: &mut FontVec,
444 weight: FontWeight,
445 style: FontStyle,
446) -> Vec<([u8; 4], f32)> {
447 const OBLIQUE_DEGREES: f32 = -12.0;
452
453 let mut applied = Vec::new();
454 for axis in font.variations() {
455 let requested = match &axis.tag {
456 b"wght" => f32::from(weight.value()),
457 b"ital" if style == FontStyle::Italic => 1.0,
458 b"slnt" if style == FontStyle::Italic => OBLIQUE_DEGREES,
459 _ => continue,
460 };
461 let value = requested.clamp(axis.min_value, axis.max_value);
462 if font.set_variation(&axis.tag, value) {
463 applied.push((axis.tag, value));
464 }
465 }
466 applied
467}
468
469fn software_text_font_metadata(bytes: &[u8]) -> SoftwareTextFontMetadata {
470 let Some(face) = ttf_parser::Face::parse(bytes, 0).ok() else {
471 return SoftwareTextFontMetadata {
472 families: Arc::from(Vec::<String>::new()),
473 registered_family: None,
474 weight: FontWeight::NORMAL,
475 style: FontStyle::Normal,
476 ab_glyph_scale_factor: 1.0,
477 };
478 };
479
480 let mut families = Vec::new();
481 for name in face.names() {
482 if matches!(
483 name.name_id,
484 ttf_parser::name_id::TYPOGRAPHIC_FAMILY | ttf_parser::name_id::FAMILY
485 ) {
486 if let Some(value) = name.to_string().filter(|value| !value.is_empty()) {
487 if !families
488 .iter()
489 .any(|existing: &String| existing.eq_ignore_ascii_case(&value))
490 {
491 families.push(value);
492 }
493 }
494 }
495 }
496 let weight = FontWeight::try_new(face.weight().to_number()).unwrap_or(FontWeight::NORMAL);
497 let style = if face.is_italic() {
498 FontStyle::Italic
499 } else {
500 FontStyle::Normal
501 };
502 let units_per_em = face.units_per_em() as f32;
503 let height = (face.ascender() as f32 - face.descender() as f32).abs();
504 let ab_glyph_scale_factor =
505 if units_per_em.is_finite() && units_per_em > 0.0 && height.is_finite() && height > 0.0 {
506 height / units_per_em
507 } else {
508 1.0
509 };
510
511 SoftwareTextFontMetadata {
512 families: Arc::from(families),
513 registered_family: None,
514 weight,
515 style,
516 ab_glyph_scale_factor,
517 }
518}
519
520#[derive(Clone)]
521struct TextMetricsKey {
522 text: Rc<str>,
523 font_size_bits: u32,
524 style_hash: u64,
525 span_styles_hash: u64,
526}
527
528impl PartialEq for TextMetricsKey {
529 fn eq(&self, other: &Self) -> bool {
530 (Rc::ptr_eq(&self.text, &other.text) || *self.text == *other.text)
531 && self.font_size_bits == other.font_size_bits
532 && self.style_hash == other.style_hash
533 && self.span_styles_hash == other.span_styles_hash
534 }
535}
536
537impl Eq for TextMetricsKey {}
538
539impl Hash for TextMetricsKey {
540 fn hash<H: Hasher>(&self, state: &mut H) {
541 self.text.hash(state);
542 self.font_size_bits.hash(state);
543 self.style_hash.hash(state);
544 self.span_styles_hash.hash(state);
545 }
546}
547
548struct SoftwareTextMetricsCache {
549 map: BoundedLruCache<TextMetricsKey, TextMetrics>,
550 line_prefix_widths: BoundedLruCache<LinePrefixWidthsKey, TextLinePrefixWidths>,
551 glyph_metrics: SoftwareTextGlyphMetricsCache,
552}
553
554impl SoftwareTextMetricsCache {
555 fn new(capacity: usize) -> Self {
556 Self {
557 map: BoundedLruCache::with_capacity_at_least_one(capacity),
558 line_prefix_widths: BoundedLruCache::with_capacity_at_least_one(
559 capacity.max(SOFTWARE_TEXT_PREFIX_WIDTH_CACHE_CAPACITY),
560 ),
561 glyph_metrics: SoftwareTextGlyphMetricsCache::new(),
562 }
563 }
564
565 fn get_or_measure(
566 &mut self,
567 fonts: &SoftwareTextFontSet,
568 text: &AnnotatedString,
569 style: &TextStyle,
570 ) -> TextMetrics {
571 let font_size = resolve_font_size(style);
572 let key = TextMetricsKey {
573 text: Rc::from(text.text.as_str()),
574 font_size_bits: font_size.to_bits(),
575 style_hash: style.measurement_hash(),
576 span_styles_hash: text.span_styles_hash(),
577 };
578 if let Some(metrics) = self.map.get(&key).copied() {
579 return metrics;
580 }
581
582 let metrics =
583 measure_annotated_text_with_font_set_cached(text, style, font_size, fonts, self);
584 self.map.put(key, metrics);
585 metrics
586 }
587
588 fn get_or_measure_line_prefix_widths(
589 &mut self,
590 fonts: &SoftwareTextFontSet,
591 text: &AnnotatedString,
592 line_range: std::ops::Range<usize>,
593 style: &TextStyle,
594 ) -> Option<TextLinePrefixWidths> {
595 let key = line_prefix_widths_key(text, line_range.clone(), style)?;
596 if let Some(widths) = self.line_prefix_widths.get(&key) {
597 return Some(widths.clone());
598 }
599
600 let widths = annotated_line_prefix_widths_with_font_set_cached(
601 text, line_range, style, fonts, self,
602 )?;
603 self.line_prefix_widths.put(key, widths.clone());
604 Some(widths)
605 }
606
607 fn get_or_measure_line_width(
608 &mut self,
609 fonts: &SoftwareTextFontSet,
610 text: &AnnotatedString,
611 line_range: std::ops::Range<usize>,
612 style: &TextStyle,
613 ) -> Option<f32> {
614 let key = line_prefix_widths_key(text, line_range.clone(), style)?;
615 if let Some(widths) = self.line_prefix_widths.get(&key) {
616 return widths.width_for_char_range(0, widths.char_count());
617 }
618
619 let widths = annotated_line_prefix_widths_with_font_set_cached(
620 text, line_range, style, fonts, self,
621 )?;
622 let width = widths.width_for_char_range(0, widths.char_count());
623 self.line_prefix_widths.put(key, widths);
624 width
625 }
626}
627
628#[derive(Clone)]
629struct LinePrefixWidthsKey {
630 text: Rc<str>,
631 start: usize,
632 end: usize,
633 style_hash: u64,
634 span_styles_hash: u64,
635}
636
637impl PartialEq for LinePrefixWidthsKey {
638 fn eq(&self, other: &Self) -> bool {
639 (Rc::ptr_eq(&self.text, &other.text) || *self.text == *other.text)
640 && self.start == other.start
641 && self.end == other.end
642 && self.style_hash == other.style_hash
643 && self.span_styles_hash == other.span_styles_hash
644 }
645}
646
647impl Eq for LinePrefixWidthsKey {}
648
649impl Hash for LinePrefixWidthsKey {
650 fn hash<H: Hasher>(&self, state: &mut H) {
651 self.text.hash(state);
652 self.start.hash(state);
653 self.end.hash(state);
654 self.style_hash.hash(state);
655 self.span_styles_hash.hash(state);
656 }
657}
658
659fn line_prefix_widths_key(
660 text: &AnnotatedString,
661 line_range: std::ops::Range<usize>,
662 style: &TextStyle,
663) -> Option<LinePrefixWidthsKey> {
664 if !style_allows_prefix_widths(style)
665 || line_range.start > line_range.end
666 || line_range.end > text.text.len()
667 || !text.text.is_char_boundary(line_range.start)
668 || !text.text.is_char_boundary(line_range.end)
669 || text.text[line_range.clone()].contains('\n')
670 {
671 return None;
672 }
673
674 Some(LinePrefixWidthsKey {
675 text: Rc::from(text.text.as_str()),
676 start: line_range.start,
677 end: line_range.end,
678 style_hash: style.measurement_hash(),
679 span_styles_hash: text.span_styles_hash(),
680 })
681}
682
683#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
684struct FontScaleMetricsKey {
685 font_hash: u64,
686 glyph_font_size_bits: u32,
687}
688
689#[derive(Clone, Copy, Debug)]
690struct CachedGlyphMetrics {
691 glyph_id: GlyphId,
692 advance: f32,
693}
694
695#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
696struct GlyphMetricsKey {
697 font: FontScaleMetricsKey,
698 ch: char,
699}
700
701#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
702struct KernMetricsKey {
703 font: FontScaleMetricsKey,
704 previous_id: u32,
705 glyph_id: u32,
706}
707
708#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
709struct SoftwareTextGlyphMetricsStats {
710 glyph_hits: u64,
711 glyph_misses: u64,
712 kern_hits: u64,
713 kern_misses: u64,
714}
715
716struct SoftwareTextGlyphMetricsCache {
717 glyphs: BoundedLruCache<GlyphMetricsKey, CachedGlyphMetrics>,
718 kerns: BoundedLruCache<KernMetricsKey, f32>,
719 stats: SoftwareTextGlyphMetricsStats,
720}
721
722impl SoftwareTextGlyphMetricsCache {
723 fn new() -> Self {
724 Self {
725 glyphs: BoundedLruCache::with_capacity_at_least_one(
726 SOFTWARE_TEXT_GLYPH_METRICS_CACHE_CAPACITY,
727 ),
728 kerns: BoundedLruCache::with_capacity_at_least_one(
729 SOFTWARE_TEXT_KERN_METRICS_CACHE_CAPACITY,
730 ),
731 stats: SoftwareTextGlyphMetricsStats::default(),
732 }
733 }
734
735 #[cfg(test)]
736 fn stats(&self) -> SoftwareTextGlyphMetricsStats {
737 self.stats
738 }
739
740 fn glyph_metrics<F, S>(
741 &mut self,
742 font: &SoftwareTextFont,
743 glyph_font_size: f32,
744 scaled_font: &S,
745 ch: char,
746 ) -> CachedGlyphMetrics
747 where
748 F: Font,
749 S: ScaleFont<F>,
750 {
751 let font_key = FontScaleMetricsKey {
752 font_hash: font.content_hash(),
753 glyph_font_size_bits: glyph_font_size.to_bits(),
754 };
755 let key = GlyphMetricsKey { font: font_key, ch };
756 if let Some(metrics) = self.glyphs.get(&key).copied() {
757 self.stats.glyph_hits = self.stats.glyph_hits.saturating_add(1);
758 return metrics;
759 }
760
761 let glyph_id = scaled_font.glyph_id(ch);
762 let metrics = CachedGlyphMetrics {
763 glyph_id,
764 advance: scaled_font.h_advance(glyph_id).max(0.0),
765 };
766 self.glyphs.put(key, metrics);
767 self.stats.glyph_misses = self.stats.glyph_misses.saturating_add(1);
768 metrics
769 }
770
771 fn kern<F, S>(
772 &mut self,
773 font: &SoftwareTextFont,
774 glyph_font_size: f32,
775 scaled_font: &S,
776 previous_id: GlyphId,
777 glyph_id: GlyphId,
778 ) -> f32
779 where
780 F: Font,
781 S: ScaleFont<F>,
782 {
783 let font_key = FontScaleMetricsKey {
784 font_hash: font.content_hash(),
785 glyph_font_size_bits: glyph_font_size.to_bits(),
786 };
787 let key = KernMetricsKey {
788 font: font_key,
789 previous_id: previous_id.0.into(),
790 glyph_id: glyph_id.0.into(),
791 };
792 if let Some(kern) = self.kerns.get(&key).copied() {
793 self.stats.kern_hits = self.stats.kern_hits.saturating_add(1);
794 return kern;
795 }
796
797 let kern = scaled_font.kern(previous_id, glyph_id);
798 self.kerns.put(key, kern);
799 self.stats.kern_misses = self.stats.kern_misses.saturating_add(1);
800 kern
801 }
802}
803
804pub struct SoftwareTextMeasurer {
805 fonts: SoftwareTextFontSet,
806 cache: Mutex<SoftwareTextMetricsCache>,
807 hyphenation: HyphenationDictionaryStore,
808}
809
810impl SoftwareTextMeasurer {
811 pub fn new(font: SoftwareTextFont, cache_capacity: usize) -> Self {
812 Self::from_font_set(SoftwareTextFontSet::from_font(font), cache_capacity)
813 }
814
815 pub fn from_font_set(fonts: SoftwareTextFontSet, cache_capacity: usize) -> Self {
816 Self {
817 fonts,
818 cache: Mutex::new(SoftwareTextMetricsCache::new(cache_capacity)),
819 hyphenation: HyphenationDictionaryStore::new(),
820 }
821 }
822
823 pub fn from_fonts_or_default(fonts: &[&[u8]], cache_capacity: usize) -> Self {
824 Self::from_font_set(
825 software_text_font_set_from_fonts_or_default(fonts),
826 cache_capacity,
827 )
828 }
829
830 fn lock_cache(&self) -> MutexGuard<'_, SoftwareTextMetricsCache> {
831 self.cache
832 .lock()
833 .unwrap_or_else(|poisoned| poisoned.into_inner())
834 }
835
836 #[cfg(feature = "text-hyphenation")]
837 pub fn register_hyphenation_dictionary_path(
838 &self,
839 locale: &str,
840 path: impl AsRef<std::path::Path>,
841 ) -> Result<(), HyphenationDictionaryError> {
842 self.hyphenation.register_dictionary_path(locale, path)
843 }
844
845 #[cfg(feature = "text-hyphenation")]
846 pub fn register_hyphenation_dictionary_reader(
847 &self,
848 locale: &str,
849 reader: &mut impl std::io::Read,
850 ) -> Result<(), HyphenationDictionaryError> {
851 self.hyphenation.register_dictionary_reader(locale, reader)
852 }
853}
854
855impl TextMeasurer for SoftwareTextMeasurer {
856 fn measure(&self, text: &cranpose_ui::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
857 self.lock_cache().get_or_measure(&self.fonts, text, style)
858 }
859
860 fn measure_subsequence(
861 &self,
862 text: &cranpose_ui::text::AnnotatedString,
863 range: std::ops::Range<usize>,
864 style: &TextStyle,
865 ) -> TextMetrics {
866 let text = text.subsequence(range);
867 self.lock_cache().get_or_measure(&self.fonts, &text, style)
868 }
869
870 fn measure_line_prefix_widths(
871 &self,
872 text: &cranpose_ui::text::AnnotatedString,
873 line_range: std::ops::Range<usize>,
874 style: &TextStyle,
875 ) -> Option<TextLinePrefixWidths> {
876 self.lock_cache()
877 .get_or_measure_line_prefix_widths(&self.fonts, text, line_range, style)
878 }
879
880 fn measure_line_width(
881 &self,
882 text: &cranpose_ui::text::AnnotatedString,
883 line_range: std::ops::Range<usize>,
884 style: &TextStyle,
885 ) -> Option<f32> {
886 self.lock_cache()
887 .get_or_measure_line_width(&self.fonts, text, line_range, style)
888 }
889
890 fn line_height(&self, text: &cranpose_ui::text::AnnotatedString, style: &TextStyle) -> f32 {
891 let font_size = resolve_font_size(style);
892 max_line_height_for_annotated_text_with_resolver(text, style, font_size, &self.fonts)
893 }
894
895 fn glyph_line_box(&self, style: &TextStyle) -> Option<(f32, f32)> {
896 let font = self.fonts.resolve(style)?;
897 let font_size = resolve_font_size(style);
898 let metrics = crate::font_layout::vertical_metrics(&font.font, font_size);
899 let line_height = line_height_for_render_style(style, font_size);
900 let height = metrics.natural_line_height.min(line_height).max(1.0);
903 Some((((line_height - height) * 0.5).max(0.0), height))
904 }
905
906 fn first_baseline(&self, style: &TextStyle) -> Option<f32> {
907 let font = self.fonts.resolve(style)?;
908 let font_size = resolve_font_size(style);
909 let metrics =
915 crate::font_layout::vertical_metrics(&font.font, font.ab_glyph_px_size(font_size));
916 Some(baseline_y_for_line_box(
917 metrics,
918 line_height_for_render_style(style, font_size),
919 ))
920 }
921
922 fn get_offset_for_position(
923 &self,
924 text: &cranpose_ui::text::AnnotatedString,
925 style: &TextStyle,
926 x: f32,
927 y: f32,
928 ) -> usize {
929 if let Some(font) = self.fonts.resolve(style) {
930 text_offset_for_position_with_font(text.text.as_str(), style, x, y, font)
931 } else {
932 fallback_text_offset_for_position(text.text.as_str(), style, x, y)
933 }
934 }
935
936 fn get_cursor_x_for_offset(
937 &self,
938 text: &cranpose_ui::text::AnnotatedString,
939 style: &TextStyle,
940 offset: usize,
941 ) -> f32 {
942 if let Some(font) = self.fonts.resolve(style) {
943 cursor_x_for_offset_with_font(text.text.as_str(), style, offset, font)
944 } else {
945 fallback_cursor_x_for_offset(text.text.as_str(), style, offset)
946 }
947 }
948
949 fn layout(
950 &self,
951 text: &cranpose_ui::text::AnnotatedString,
952 style: &TextStyle,
953 ) -> TextLayoutResult {
954 if let Some(font) = self.fonts.resolve(style) {
955 layout_text_with_font(text.text.as_str(), style, font)
956 } else {
957 fallback_layout_text(text.text.as_str(), style)
958 }
959 }
960
961 fn choose_auto_hyphen_break(
962 &self,
963 line: &str,
964 style: &TextStyle,
965 segment_start_char: usize,
966 measured_break_char: usize,
967 ) -> Option<usize> {
968 self.hyphenation.choose_auto_hyphen_break(
969 line,
970 style,
971 segment_start_char,
972 measured_break_char,
973 )
974 }
975}
976
977pub fn software_text_content_hash(text: &cranpose_ui::text::AnnotatedString) -> u64 {
978 let mut state = default_hash::new();
979 text.text.hash(&mut state);
980 text.span_styles_hash().hash(&mut state);
981 state.finish()
982}
983
984#[derive(Clone, Copy)]
985enum GlyphRasterStyle {
986 Fill,
987 Stroke { width_px: f32 },
988}
989
990#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
991pub struct SoftwareGlyphAtlasKey {
992 pub font_hash: u64,
993 pub glyph_id: u32,
994 pub scale_x_bits: u32,
995 pub scale_y_bits: u32,
996 pub embolden_px_bits: u32,
997 pub slant_bits: u32,
998}
999
1000#[derive(Clone)]
1001pub struct SoftwareGlyphAtlasMask {
1002 pub alpha: Arc<[f32]>,
1003 pub width: usize,
1004 pub height: usize,
1005}
1006
1007#[derive(Clone)]
1008pub struct SoftwareGlyphAtlasGlyph {
1009 pub key: SoftwareGlyphAtlasKey,
1010 pub mask: SoftwareGlyphAtlasMask,
1011 pub x: i32,
1012 pub y: i32,
1013 pub color: Color,
1014}
1015
1016#[derive(Clone, Copy)]
1017pub struct SoftwareGlyphAtlasPlacement {
1018 pub key: SoftwareGlyphAtlasKey,
1019 pub x: i32,
1020 pub y: i32,
1021 pub width: usize,
1022 pub height: usize,
1023 pub color: Color,
1024}
1025
1026#[derive(Clone)]
1027pub enum SoftwareGlyphAtlasRunGlyph {
1028 Cached(SoftwareGlyphAtlasPlacement),
1029 New(SoftwareGlyphAtlasGlyph),
1030}
1031
1032impl SoftwareGlyphAtlasRunGlyph {
1033 pub fn placement(&self) -> SoftwareGlyphAtlasPlacement {
1034 match self {
1035 Self::Cached(placement) => *placement,
1036 Self::New(glyph) => SoftwareGlyphAtlasPlacement {
1037 key: glyph.key,
1038 x: glyph.x,
1039 y: glyph.y,
1040 width: glyph.mask.width,
1041 height: glyph.mask.height,
1042 color: glyph.color,
1043 },
1044 }
1045 }
1046}
1047
1048#[derive(Clone)]
1049struct GlyphMask {
1050 alpha: Arc<[f32]>,
1051 width: usize,
1052 height: usize,
1053 origin_x: i32,
1054 origin_y: i32,
1055}
1056
1057#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1058pub struct SoftwareGlyphRasterCacheStats {
1059 pub entries: usize,
1060 pub hits: u64,
1061 pub misses: u64,
1062}
1063
1064const RUN_GLYPH_METRICS_CACHE_LIMIT: usize = 64;
1065
1066#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1067enum GlyphRasterStyleKey {
1068 Fill,
1069 Stroke { width_px_bits: u32 },
1070}
1071
1072impl GlyphRasterStyleKey {
1073 fn from_style(style: GlyphRasterStyle) -> Self {
1074 match style {
1075 GlyphRasterStyle::Fill => Self::Fill,
1076 GlyphRasterStyle::Stroke { width_px } => Self::Stroke {
1077 width_px_bits: width_px.to_bits(),
1078 },
1079 }
1080 }
1081}
1082
1083#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1084struct GlyphMaskCacheKey {
1085 font_hash: u64,
1086 glyph_id: u32,
1087 scale_x_bits: u32,
1088 scale_y_bits: u32,
1089 raster_style: GlyphRasterStyleKey,
1090 embolden_px_bits: u32,
1091 slant_bits: u32,
1092}
1093
1094#[derive(Clone)]
1095struct CachedGlyphMask {
1096 alpha: Arc<[f32]>,
1097 width: usize,
1098 height: usize,
1099 origin_offset_x: i32,
1100 origin_offset_y: i32,
1101}
1102
1103impl CachedGlyphMask {
1104 fn from_mask(mask: GlyphMask, glyph: &Glyph) -> Self {
1105 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1106 Self {
1107 alpha: mask.alpha,
1108 width: mask.width,
1109 height: mask.height,
1110 origin_offset_x: mask.origin_x - glyph_x,
1111 origin_offset_y: mask.origin_y - glyph_y,
1112 }
1113 }
1114
1115 fn instantiate(&self, glyph: &Glyph) -> GlyphMask {
1116 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1117 GlyphMask {
1118 alpha: Arc::clone(&self.alpha),
1119 width: self.width,
1120 height: self.height,
1121 origin_x: glyph_x + self.origin_offset_x,
1122 origin_y: glyph_y + self.origin_offset_y,
1123 }
1124 }
1125
1126 fn placement(&self, glyph: &Glyph) -> (i32, i32, usize, usize) {
1127 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1128 (
1129 glyph_x + self.origin_offset_x,
1130 glyph_y + self.origin_offset_y,
1131 self.width,
1132 self.height,
1133 )
1134 }
1135
1136 fn atlas_metrics(&self, key: SoftwareGlyphAtlasKey) -> CachedAtlasGlyphMetrics {
1137 CachedAtlasGlyphMetrics {
1138 key,
1139 width: self.width,
1140 height: self.height,
1141 origin_offset_x: self.origin_offset_x,
1142 origin_offset_y: self.origin_offset_y,
1143 }
1144 }
1145}
1146
1147#[derive(Clone, Copy)]
1148struct CachedAtlasGlyphMetrics {
1149 key: SoftwareGlyphAtlasKey,
1150 width: usize,
1151 height: usize,
1152 origin_offset_x: i32,
1153 origin_offset_y: i32,
1154}
1155
1156impl CachedAtlasGlyphMetrics {
1157 fn placement(self, glyph: &Glyph, color: Color) -> SoftwareGlyphAtlasPlacement {
1158 let (glyph_x, glyph_y) = static_glyph_pixel_origin(glyph);
1159 SoftwareGlyphAtlasPlacement {
1160 key: self.key,
1161 x: glyph_x + self.origin_offset_x,
1162 y: glyph_y + self.origin_offset_y,
1163 width: self.width,
1164 height: self.height,
1165 color,
1166 }
1167 }
1168}
1169
1170pub struct SoftwareGlyphRasterCache {
1171 masks: BoundedLruCache<GlyphMaskCacheKey, CachedGlyphMask>,
1172 hits: u64,
1173 misses: u64,
1174}
1175
1176impl SoftwareGlyphRasterCache {
1177 pub fn with_capacity_at_least_one(capacity: usize) -> Self {
1178 Self {
1179 masks: BoundedLruCache::with_capacity_at_least_one(capacity),
1180 hits: 0,
1181 misses: 0,
1182 }
1183 }
1184
1185 pub fn stats(&self) -> SoftwareGlyphRasterCacheStats {
1186 SoftwareGlyphRasterCacheStats {
1187 entries: self.masks.len(),
1188 hits: self.hits,
1189 misses: self.misses,
1190 }
1191 }
1192
1193 fn get(&mut self, key: &GlyphMaskCacheKey, glyph: &Glyph) -> Option<GlyphMask> {
1194 let mask = self.masks.get(key)?.instantiate(glyph);
1195 self.hits = self.hits.saturating_add(1);
1196 Some(mask)
1197 }
1198
1199 fn get_atlas_placement(
1200 &mut self,
1201 key: &GlyphMaskCacheKey,
1202 glyph: &Glyph,
1203 ) -> Option<(SoftwareGlyphAtlasKey, i32, i32, usize, usize)> {
1204 let atlas_key = glyph_atlas_key_from_mask_key(*key)?;
1205 let (x, y, width, height) = self.masks.get(key)?.placement(glyph);
1206 self.hits = self.hits.saturating_add(1);
1207 Some((atlas_key, x, y, width, height))
1208 }
1209
1210 fn get_atlas_metrics(&mut self, key: &GlyphMaskCacheKey) -> Option<CachedAtlasGlyphMetrics> {
1211 let atlas_key = glyph_atlas_key_from_mask_key(*key)?;
1212 let metrics = self.masks.get(key)?.atlas_metrics(atlas_key);
1213 self.hits = self.hits.saturating_add(1);
1214 Some(metrics)
1215 }
1216
1217 pub fn atlas_glyph_for_placement(
1218 &mut self,
1219 placement: &SoftwareGlyphAtlasPlacement,
1220 ) -> Option<SoftwareGlyphAtlasGlyph> {
1221 let key = GlyphMaskCacheKey {
1222 font_hash: placement.key.font_hash,
1223 glyph_id: placement.key.glyph_id,
1224 scale_x_bits: placement.key.scale_x_bits,
1225 scale_y_bits: placement.key.scale_y_bits,
1226 raster_style: GlyphRasterStyleKey::Fill,
1227 embolden_px_bits: placement.key.embolden_px_bits,
1228 slant_bits: placement.key.slant_bits,
1229 };
1230 let mask = self.masks.get(&key)?;
1231 self.hits = self.hits.saturating_add(1);
1232 Some(SoftwareGlyphAtlasGlyph {
1233 key: placement.key,
1234 mask: SoftwareGlyphAtlasMask {
1235 alpha: Arc::clone(&mask.alpha),
1236 width: mask.width,
1237 height: mask.height,
1238 },
1239 x: placement.x,
1240 y: placement.y,
1241 color: placement.color,
1242 })
1243 }
1244
1245 fn put(&mut self, key: GlyphMaskCacheKey, glyph: &Glyph, mask: GlyphMask) -> GlyphMask {
1246 let cached = CachedGlyphMask::from_mask(mask, glyph);
1247 let mask = cached.instantiate(glyph);
1248 self.masks.put(key, cached);
1249 self.misses = self.misses.saturating_add(1);
1250 mask
1251 }
1252}
1253
1254struct RasterFontRef<'a, F> {
1255 font: &'a F,
1256 ab_glyph_scale_factor: f32,
1257 weight: FontWeight,
1258 style: FontStyle,
1259}
1260
1261#[derive(Clone, Copy)]
1262struct TextWeightSynthesis {
1263 embolden_px: f32,
1264 advance_scale: f32,
1265}
1266
1267impl TextWeightSynthesis {
1268 fn none() -> Self {
1269 Self {
1270 embolden_px: 0.0,
1271 advance_scale: 1.0,
1272 }
1273 }
1274
1275 fn for_style(
1276 style: &TextStyle,
1277 resolved_weight: FontWeight,
1278 font_size: f32,
1279 scale: f32,
1280 ) -> Self {
1281 let requested_weight = style.span_style.font_weight.unwrap_or_default();
1282 if requested_weight <= resolved_weight {
1283 return Self::none();
1284 }
1285
1286 let synthesis = style
1287 .span_style
1288 .font_synthesis
1289 .unwrap_or(FontSynthesis::All);
1290 if !matches!(synthesis, FontSynthesis::All | FontSynthesis::Weight) {
1291 return Self::none();
1292 }
1293
1294 let weight_delta = (requested_weight.value() - resolved_weight.value()) as f32;
1295 let strength = (weight_delta / 300.0).clamp(0.0, 1.5);
1296 Self {
1297 embolden_px: (font_size * scale * 0.055 * strength).clamp(0.0, 3.0 * scale),
1298 advance_scale: 1.0 + 0.085 * strength.min(1.0),
1299 }
1300 }
1301
1302 fn apply_width(self, width: f32) -> f32 {
1303 width * self.advance_scale
1304 }
1305}
1306
1307#[derive(Clone, Copy)]
1308struct TextStyleSynthesis {
1309 slant: f32,
1310 font_size: f32,
1311 scale: f32,
1312}
1313
1314impl TextStyleSynthesis {
1315 fn none() -> Self {
1316 Self {
1317 slant: 0.0,
1318 font_size: 0.0,
1319 scale: 1.0,
1320 }
1321 }
1322
1323 fn for_style(style: &TextStyle, resolved_style: FontStyle, font_size: f32, scale: f32) -> Self {
1324 let requested_style = style.span_style.font_style.unwrap_or_default();
1325 if requested_style != FontStyle::Italic || resolved_style == FontStyle::Italic {
1326 return Self::none();
1327 }
1328
1329 let synthesis = style
1330 .span_style
1331 .font_synthesis
1332 .unwrap_or(FontSynthesis::All);
1333 if !matches!(synthesis, FontSynthesis::All | FontSynthesis::Style) {
1334 return Self::none();
1335 }
1336
1337 Self {
1338 slant: 0.22,
1339 font_size,
1340 scale,
1341 }
1342 }
1343
1344 fn visual_overhang_px(self) -> f32 {
1345 if self.slant <= 0.0 || !self.font_size.is_finite() || !self.scale.is_finite() {
1346 return 0.0;
1347 }
1348 (self.font_size * self.scale * self.slant).ceil().max(0.0)
1349 }
1350}
1351
1352pub fn rasterize_text_to_image(
1353 text: &str,
1354 rect: Rect,
1355 style: &TextStyle,
1356 fallback_color: Color,
1357 font_size: f32,
1358 scale: f32,
1359 font: &SoftwareTextFont,
1360) -> Option<ImageBitmap> {
1361 rasterize_text_to_image_impl(
1362 TextRasterImageRequest {
1363 text,
1364 rect,
1365 style,
1366 fallback_color,
1367 font_size,
1368 scale,
1369 },
1370 RasterFontRef {
1371 font: &font.font,
1372 ab_glyph_scale_factor: font.metadata.ab_glyph_scale_factor,
1373 weight: font.weight(),
1374 style: font.style(),
1375 },
1376 font.content_hash(),
1377 None,
1378 )
1379}
1380
1381#[allow(clippy::too_many_arguments)]
1382pub fn rasterize_text_to_image_with_glyph_cache(
1383 text: &str,
1384 rect: Rect,
1385 style: &TextStyle,
1386 fallback_color: Color,
1387 font_size: f32,
1388 scale: f32,
1389 font: &SoftwareTextFont,
1390 glyph_cache: &mut SoftwareGlyphRasterCache,
1391) -> Option<ImageBitmap> {
1392 rasterize_text_to_image_impl(
1393 TextRasterImageRequest {
1394 text,
1395 rect,
1396 style,
1397 fallback_color,
1398 font_size,
1399 scale,
1400 },
1401 RasterFontRef {
1402 font: &font.font,
1403 ab_glyph_scale_factor: font.metadata.ab_glyph_scale_factor,
1404 weight: font.weight(),
1405 style: font.style(),
1406 },
1407 font.content_hash(),
1408 Some(glyph_cache),
1409 )
1410}
1411
1412#[derive(Clone, Copy)]
1417pub struct StyledTextRef<'a> {
1418 pub text: &'a str,
1419 pub span_styles: &'a [RangeStyle<SpanStyle>],
1420}
1421
1422impl StyledTextRef<'_> {
1423 fn is_empty(&self) -> bool {
1424 self.text.is_empty()
1425 }
1426
1427 fn span_boundaries(&self) -> Vec<usize> {
1429 let mut boundaries = vec![0, self.text.len()];
1430 for span in self.span_styles {
1431 boundaries.push(span.range.start);
1432 boundaries.push(span.range.end);
1433 }
1434 boundaries.sort_unstable();
1435 boundaries.dedup();
1436 boundaries
1437 .into_iter()
1438 .filter(|&b| b <= self.text.len() && self.text.is_char_boundary(b))
1439 .collect()
1440 }
1441}
1442
1443impl<'a> From<&'a AnnotatedString> for StyledTextRef<'a> {
1444 fn from(text: &'a AnnotatedString) -> Self {
1445 Self {
1446 text: text.text.as_str(),
1447 span_styles: &text.span_styles,
1448 }
1449 }
1450}
1451
1452impl<'a> From<&'a RenderString> for StyledTextRef<'a> {
1453 fn from(text: &'a RenderString) -> Self {
1454 Self {
1455 text: text.text.as_str(),
1456 span_styles: &text.span_styles,
1457 }
1458 }
1459}
1460
1461#[allow(clippy::too_many_arguments)]
1462pub fn rasterize_annotated_text_to_image_with_glyph_cache<'a>(
1463 text: impl Into<StyledTextRef<'a>>,
1464 rect: Rect,
1465 style: &TextStyle,
1466 fallback_color: Color,
1467 font_size: f32,
1468 scale: f32,
1469 fonts: &SoftwareTextFontSet,
1470 glyph_cache: &mut SoftwareGlyphRasterCache,
1471) -> Option<ImageBitmap> {
1472 let text: StyledTextRef<'a> = text.into();
1473 if text.span_styles.is_empty() {
1474 let font = fonts.resolve(style)?;
1475 return rasterize_text_to_image_with_glyph_cache(
1476 text.text,
1477 rect,
1478 style,
1479 fallback_color,
1480 font_size,
1481 scale,
1482 font,
1483 glyph_cache,
1484 );
1485 }
1486 if text.is_empty()
1487 || rect.width <= 0.0
1488 || rect.height <= 0.0
1489 || !font_size.is_finite()
1490 || font_size <= 0.0
1491 || !scale.is_finite()
1492 || scale <= 0.0
1493 {
1494 return None;
1495 }
1496
1497 let width = rect.width.ceil().max(1.0) as u32;
1498 let height = rect.height.ceil().max(1.0) as u32;
1499 let boundaries = text.span_boundaries();
1500 let mut segment_plan = Vec::with_capacity(boundaries.len().saturating_sub(1));
1501 for window in boundaries.windows(2) {
1502 let start = window[0];
1503 let end = window[1];
1504 if start == end {
1505 continue;
1506 }
1507 let segment_style = effective_style_for_range(text.span_styles, style, start, end);
1508 if !style_can_rasterize_direct_solid(&segment_style) {
1509 return None;
1510 }
1511 let static_text_motion = segment_style
1512 .paragraph_style
1513 .text_motion
1514 .unwrap_or(TextMotion::Static)
1515 == TextMotion::Static;
1516 if !static_text_motion {
1517 return None;
1518 }
1519 segment_plan.push((start, end, segment_style));
1520 }
1521
1522 let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
1523 let base_line_height = line_height_for_render_style(style, font_size);
1524 let mut current_line_height = base_line_height;
1525 let mut cursor_x = rect.x;
1526 let mut cursor_y = rect.y;
1527
1528 for (start, end, segment_style) in segment_plan {
1529 let segment = &text.text[start..end];
1530 for part in segment.split_inclusive('\n') {
1531 let has_newline = part.ends_with('\n');
1532 let content = if has_newline {
1533 &part[..part.len().saturating_sub(1)]
1534 } else {
1535 part
1536 };
1537
1538 if !content.is_empty() {
1539 let segment_font_size = segment_style.resolve_font_size(font_size);
1540 if let Some(font) = fonts.resolve(&segment_style) {
1541 let local_rect = Rect {
1542 x: (cursor_x - rect.x).round(),
1543 y: (cursor_y - rect.y).round(),
1544 width: width as f32,
1545 height: height as f32,
1546 };
1547 let color = segment_style.resolve_text_color(fallback_color);
1548 let advance_px = draw_text_segment_solid_to_rgba(
1549 &mut canvas,
1550 width,
1551 height,
1552 content,
1553 local_rect,
1554 &segment_style,
1555 color,
1556 segment_font_size,
1557 scale,
1558 font,
1559 glyph_cache,
1560 );
1561 cursor_x += advance_px;
1562 current_line_height = current_line_height.max(line_height_for_render_style(
1563 &segment_style,
1564 segment_font_size,
1565 ));
1566 }
1567 }
1568
1569 if has_newline {
1570 cursor_x = rect.x;
1571 cursor_y += current_line_height * scale;
1572 current_line_height = base_line_height;
1573 }
1574 }
1575 }
1576
1577 ImageBitmap::from_rgba8(width, height, canvas).ok()
1578}
1579
1580#[allow(clippy::too_many_arguments)]
1581pub fn collect_solid_text_atlas_glyphs(
1582 text: &AnnotatedString,
1583 rect: Rect,
1584 style: &TextStyle,
1585 fallback_color: Color,
1586 font_size: f32,
1587 scale: f32,
1588 fonts: &SoftwareTextFontSet,
1589 glyph_cache: &mut SoftwareGlyphRasterCache,
1590 out: &mut Vec<SoftwareGlyphAtlasGlyph>,
1591) -> Option<()> {
1592 if text.is_empty()
1593 || rect.width <= 0.0
1594 || rect.height <= 0.0
1595 || !font_size.is_finite()
1596 || font_size <= 0.0
1597 || !scale.is_finite()
1598 || scale <= 0.0
1599 {
1600 return Some(());
1601 }
1602
1603 let base_line_height = line_height_for_render_style(style, font_size);
1604 let mut current_line_height = base_line_height;
1605 let mut cursor_x = rect.x;
1606 let mut cursor_y = rect.y;
1607 let initial_len = out.len();
1608
1609 let mut boundaries = text.span_boundaries();
1610 for (offset, ch) in text.text.char_indices() {
1611 if ch == '\n' {
1612 boundaries.push(offset);
1613 boundaries.push(offset + ch.len_utf8());
1614 }
1615 }
1616 boundaries.sort_unstable();
1617 boundaries.dedup();
1618 boundaries.retain(|offset| *offset <= text.text.len() && text.text.is_char_boundary(*offset));
1619
1620 for range in boundaries.windows(2) {
1621 let start = range[0];
1622 let end = range[1];
1623 if start == end {
1624 continue;
1625 }
1626 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
1627 if !style_can_atlas_solid_fill(&segment_style) {
1628 out.truncate(initial_len);
1629 return None;
1630 }
1631 let static_text_motion = segment_style
1632 .paragraph_style
1633 .text_motion
1634 .unwrap_or(TextMotion::Static)
1635 == TextMotion::Static;
1636 if !static_text_motion {
1637 out.truncate(initial_len);
1638 return None;
1639 }
1640
1641 let segment = &text.text[start..end];
1642 for part in segment.split_inclusive('\n') {
1643 let has_newline = part.ends_with('\n');
1644 let content = if has_newline {
1645 &part[..part.len().saturating_sub(1)]
1646 } else {
1647 part
1648 };
1649
1650 if !content.is_empty() {
1651 let segment_font_size = segment_style.resolve_font_size(font_size);
1652 let Some(font) = fonts.resolve(&segment_style) else {
1653 out.truncate(initial_len);
1654 return None;
1655 };
1656 let local_rect = Rect {
1657 x: (cursor_x - rect.x).round(),
1658 y: (cursor_y - rect.y).round(),
1659 width: rect.width,
1660 height: rect.height,
1661 };
1662 let color = segment_style.resolve_text_color(fallback_color);
1663 let advance_px = collect_text_segment_solid_atlas_glyphs(
1664 content,
1665 local_rect,
1666 &segment_style,
1667 color,
1668 segment_font_size,
1669 scale,
1670 font,
1671 glyph_cache,
1672 out,
1673 )?;
1674 cursor_x += advance_px;
1675 current_line_height = current_line_height.max(line_height_for_render_style(
1676 &segment_style,
1677 segment_font_size,
1678 ));
1679 }
1680
1681 if has_newline {
1682 cursor_x = rect.x;
1683 cursor_y += current_line_height * scale;
1684 current_line_height = base_line_height;
1685 }
1686 }
1687 }
1688
1689 Some(())
1690}
1691
1692#[allow(clippy::too_many_arguments)]
1693pub fn collect_cached_solid_text_atlas_placements(
1694 text: &AnnotatedString,
1695 rect: Rect,
1696 style: &TextStyle,
1697 fallback_color: Color,
1698 font_size: f32,
1699 scale: f32,
1700 fonts: &SoftwareTextFontSet,
1701 glyph_cache: &mut SoftwareGlyphRasterCache,
1702 out: &mut Vec<SoftwareGlyphAtlasPlacement>,
1703) -> Option<()> {
1704 if text.is_empty()
1705 || rect.width <= 0.0
1706 || rect.height <= 0.0
1707 || !font_size.is_finite()
1708 || font_size <= 0.0
1709 || !scale.is_finite()
1710 || scale <= 0.0
1711 {
1712 return Some(());
1713 }
1714
1715 let base_line_height = line_height_for_render_style(style, font_size);
1716 let mut current_line_height = base_line_height;
1717 let mut cursor_x = rect.x;
1718 let mut cursor_y = rect.y;
1719 let initial_len = out.len();
1720
1721 let mut boundaries = text.span_boundaries();
1722 for (offset, ch) in text.text.char_indices() {
1723 if ch == '\n' {
1724 boundaries.push(offset);
1725 boundaries.push(offset + ch.len_utf8());
1726 }
1727 }
1728 boundaries.sort_unstable();
1729 boundaries.dedup();
1730 boundaries.retain(|offset| *offset <= text.text.len() && text.text.is_char_boundary(*offset));
1731
1732 for range in boundaries.windows(2) {
1733 let start = range[0];
1734 let end = range[1];
1735 if start == end {
1736 continue;
1737 }
1738 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
1739 if !style_can_atlas_solid_fill(&segment_style) {
1740 out.truncate(initial_len);
1741 return None;
1742 }
1743 let static_text_motion = segment_style
1744 .paragraph_style
1745 .text_motion
1746 .unwrap_or(TextMotion::Static)
1747 == TextMotion::Static;
1748 if !static_text_motion {
1749 out.truncate(initial_len);
1750 return None;
1751 }
1752
1753 let segment = &text.text[start..end];
1754 for part in segment.split_inclusive('\n') {
1755 let has_newline = part.ends_with('\n');
1756 let content = if has_newline {
1757 &part[..part.len().saturating_sub(1)]
1758 } else {
1759 part
1760 };
1761
1762 if !content.is_empty() {
1763 let segment_font_size = segment_style.resolve_font_size(font_size);
1764 let Some(font) = fonts.resolve(&segment_style) else {
1765 out.truncate(initial_len);
1766 return None;
1767 };
1768 let local_rect = Rect {
1769 x: (cursor_x - rect.x).round(),
1770 y: (cursor_y - rect.y).round(),
1771 width: rect.width,
1772 height: rect.height,
1773 };
1774 let color = segment_style.resolve_text_color(fallback_color);
1775 let advance_px = collect_text_segment_cached_solid_atlas_placements(
1776 content,
1777 local_rect,
1778 &segment_style,
1779 color,
1780 segment_font_size,
1781 scale,
1782 font,
1783 glyph_cache,
1784 out,
1785 )?;
1786 cursor_x += advance_px;
1787 current_line_height = current_line_height.max(line_height_for_render_style(
1788 &segment_style,
1789 segment_font_size,
1790 ));
1791 }
1792
1793 if has_newline {
1794 cursor_x = rect.x;
1795 cursor_y += current_line_height * scale;
1796 current_line_height = base_line_height;
1797 }
1798 }
1799 }
1800
1801 Some(())
1802}
1803
1804#[allow(clippy::too_many_arguments)]
1805pub fn collect_solid_text_atlas_run<'a>(
1806 text: impl Into<StyledTextRef<'a>>,
1807 rect: Rect,
1808 style: &TextStyle,
1809 fallback_color: Color,
1810 font_size: f32,
1811 scale: f32,
1812 fonts: &SoftwareTextFontSet,
1813 glyph_cache: &mut SoftwareGlyphRasterCache,
1814 out: &mut Vec<SoftwareGlyphAtlasRunGlyph>,
1815) -> Option<()> {
1816 let text: StyledTextRef<'a> = text.into();
1817 if text.is_empty()
1818 || rect.width <= 0.0
1819 || rect.height <= 0.0
1820 || !font_size.is_finite()
1821 || font_size <= 0.0
1822 || !scale.is_finite()
1823 || scale <= 0.0
1824 {
1825 return Some(());
1826 }
1827
1828 let base_line_height = line_height_for_render_style(style, font_size);
1829 let mut current_line_height = base_line_height;
1830 let mut cursor_x = rect.x;
1831 let mut cursor_y = rect.y;
1832 let initial_len = out.len();
1833
1834 let mut boundaries = text.span_boundaries();
1835 for (offset, ch) in text.text.char_indices() {
1836 if ch == '\n' {
1837 boundaries.push(offset);
1838 boundaries.push(offset + ch.len_utf8());
1839 }
1840 }
1841 boundaries.sort_unstable();
1842 boundaries.dedup();
1843 boundaries.retain(|offset| *offset <= text.text.len() && text.text.is_char_boundary(*offset));
1844
1845 for range in boundaries.windows(2) {
1846 let start = range[0];
1847 let end = range[1];
1848 if start == end {
1849 continue;
1850 }
1851 let segment_style = effective_style_for_range(text.span_styles, style, start, end);
1852 if !style_can_atlas_solid_fill(&segment_style) {
1853 out.truncate(initial_len);
1854 return None;
1855 }
1856 let static_text_motion = segment_style
1857 .paragraph_style
1858 .text_motion
1859 .unwrap_or(TextMotion::Static)
1860 == TextMotion::Static;
1861 if !static_text_motion {
1862 out.truncate(initial_len);
1863 return None;
1864 }
1865
1866 let segment = &text.text[start..end];
1867 for part in segment.split_inclusive('\n') {
1868 let has_newline = part.ends_with('\n');
1869 let content = if has_newline {
1870 &part[..part.len().saturating_sub(1)]
1871 } else {
1872 part
1873 };
1874
1875 if !content.is_empty() {
1876 let segment_font_size = segment_style.resolve_font_size(font_size);
1877 let Some(font) = fonts.resolve(&segment_style) else {
1878 out.truncate(initial_len);
1879 return None;
1880 };
1881 let local_rect = Rect {
1882 x: (cursor_x - rect.x).round(),
1883 y: (cursor_y - rect.y).round(),
1884 width: rect.width,
1885 height: rect.height,
1886 };
1887 let color = segment_style.resolve_text_color(fallback_color);
1888 let advance_px = collect_text_segment_solid_atlas_run(
1889 content,
1890 local_rect,
1891 &segment_style,
1892 color,
1893 segment_font_size,
1894 scale,
1895 font,
1896 glyph_cache,
1897 out,
1898 )?;
1899 cursor_x += advance_px;
1900 current_line_height = current_line_height.max(line_height_for_render_style(
1901 &segment_style,
1902 segment_font_size,
1903 ));
1904 }
1905
1906 if has_newline {
1907 cursor_x = rect.x;
1908 cursor_y += current_line_height * scale;
1909 current_line_height = base_line_height;
1910 }
1911 }
1912 }
1913
1914 Some(())
1915}
1916
1917pub fn measure_text_with_font(
1918 text: &str,
1919 style: &TextStyle,
1920 font_size: f32,
1921 font: &SoftwareTextFont,
1922) -> TextMetrics {
1923 measure_text_impl(
1924 text,
1925 style,
1926 font_size,
1927 font.ab_glyph_px_size(font_size),
1928 &font.font,
1929 font.style(),
1930 font.weight(),
1931 )
1932}
1933
1934fn measure_text_with_font_cached(
1935 text: &str,
1936 style: &TextStyle,
1937 font_size: f32,
1938 font: &SoftwareTextFont,
1939 cache: &mut SoftwareTextMetricsCache,
1940) -> TextMetrics {
1941 measure_text_impl_cached(text, style, font_size, font, cache)
1942}
1943
1944pub fn measure_annotated_text_with_font(
1945 text: &AnnotatedString,
1946 style: &TextStyle,
1947 font_size: f32,
1948 font: &SoftwareTextFont,
1949) -> TextMetrics {
1950 if text.span_styles.is_empty() {
1951 return measure_text_with_font(text.text.as_str(), style, font_size, font);
1952 }
1953 measure_annotated_text_with_resolver(
1954 text,
1955 style,
1956 font_size,
1957 &SoftwareTextFontSet::from_font(font.clone()),
1958 None,
1959 )
1960}
1961
1962pub fn measure_annotated_text_with_font_set(
1963 text: &AnnotatedString,
1964 style: &TextStyle,
1965 font_size: f32,
1966 fonts: &SoftwareTextFontSet,
1967) -> TextMetrics {
1968 if text.span_styles.is_empty() {
1969 if let Some(font) = fonts.resolve(style) {
1970 return measure_text_with_font(text.text.as_str(), style, font_size, font);
1971 }
1972 return fallback_text_metrics(text.text.as_str(), style, font_size);
1973 }
1974 measure_annotated_text_with_resolver(text, style, font_size, fonts, None)
1975}
1976
1977fn measure_annotated_text_with_font_set_cached(
1978 text: &AnnotatedString,
1979 style: &TextStyle,
1980 font_size: f32,
1981 fonts: &SoftwareTextFontSet,
1982 cache: &mut SoftwareTextMetricsCache,
1983) -> TextMetrics {
1984 if text.span_styles.is_empty() {
1985 if let Some(font) = fonts.resolve(style) {
1986 return measure_text_with_font_cached(
1987 text.text.as_str(),
1988 style,
1989 font_size,
1990 font,
1991 cache,
1992 );
1993 }
1994 return fallback_text_metrics(text.text.as_str(), style, font_size);
1995 }
1996 measure_annotated_text_with_resolver(text, style, font_size, fonts, Some(cache))
1997}
1998
1999pub fn text_offset_for_position_with_font(
2000 text: &str,
2001 style: &TextStyle,
2002 x: f32,
2003 y: f32,
2004 font: &SoftwareTextFont,
2005) -> usize {
2006 if text.is_empty() {
2007 return 0;
2008 }
2009
2010 let font_size = resolve_font_size(style);
2011 let glyph_font_size = font.ab_glyph_px_size(font_size);
2012 let line_height = resolve_line_height(style, font_size * 1.4);
2013
2014 let line_index = (y / line_height).floor().max(0.0) as usize;
2015 let lines: Vec<&str> = text.split('\n').collect();
2016 let target_line = line_index.min(lines.len().saturating_sub(1));
2017
2018 let mut line_start_byte = 0;
2019 for line in lines.iter().take(target_line) {
2020 line_start_byte += line.len() + 1;
2021 }
2022
2023 let line_text = lines.get(target_line).unwrap_or(&"");
2024 if line_text.is_empty() {
2025 return line_start_byte;
2026 }
2027
2028 let mut best_offset = 0;
2029 let mut best_distance = f32::INFINITY;
2030 let mut current_byte_offset = 0;
2031
2032 for c in line_text.chars() {
2033 let prefix = &line_text[..current_byte_offset];
2034 let glyph_x = measure_text_impl(
2035 prefix,
2036 style,
2037 font_size,
2038 glyph_font_size,
2039 &font.font,
2040 font.style(),
2041 font.weight(),
2042 )
2043 .width;
2044
2045 let char_str = &line_text[current_byte_offset..current_byte_offset + c.len_utf8()];
2046 let char_width = measure_text_impl(
2047 char_str,
2048 style,
2049 font_size,
2050 glyph_font_size,
2051 &font.font,
2052 font.style(),
2053 font.weight(),
2054 )
2055 .width
2056 .max(font_size * 0.5);
2057
2058 let left_dist = (x - glyph_x).abs();
2059 if left_dist < best_distance {
2060 best_distance = left_dist;
2061 best_offset = current_byte_offset;
2062 }
2063
2064 let right_x = glyph_x + char_width;
2065 let right_dist = (x - right_x).abs();
2066 if right_dist < best_distance {
2067 best_distance = right_dist;
2068 best_offset = current_byte_offset + c.len_utf8();
2069 }
2070
2071 current_byte_offset += c.len_utf8();
2072 }
2073
2074 let total_width = measure_text_impl(
2075 line_text,
2076 style,
2077 font_size,
2078 glyph_font_size,
2079 &font.font,
2080 font.style(),
2081 font.weight(),
2082 )
2083 .width;
2084 let end_dist = (x - total_width).abs();
2085 if end_dist < best_distance {
2086 best_offset = line_text.len();
2087 }
2088
2089 line_start_byte + best_offset.min(line_text.len())
2090}
2091
2092pub fn cursor_x_for_offset_with_font(
2093 text: &str,
2094 style: &TextStyle,
2095 offset: usize,
2096 font: &SoftwareTextFont,
2097) -> f32 {
2098 let clamped_offset = clamp_to_char_boundary(text, offset.min(text.len()));
2099 if clamped_offset == 0 {
2100 return 0.0;
2101 }
2102
2103 let font_size = resolve_font_size(style);
2104 measure_text_impl(
2105 &text[..clamped_offset],
2106 style,
2107 font_size,
2108 font.ab_glyph_px_size(font_size),
2109 &font.font,
2110 font.style(),
2111 font.weight(),
2112 )
2113 .width
2114}
2115
2116pub fn layout_text_with_font(
2117 text: &str,
2118 style: &TextStyle,
2119 font: &SoftwareTextFont,
2120) -> TextLayoutResult {
2121 let font_size = resolve_font_size(style);
2122 let glyph_font_size = font.ab_glyph_px_size(font_size);
2123 let resolved_weight = font.weight();
2124 let resolved_style = font.style();
2125 let weight_synthesis = TextWeightSynthesis::for_style(style, resolved_weight, font_size, 1.0);
2126 let font = &font.font;
2127 let line_height = resolve_line_height(style, font_size * 1.4);
2128 let letter_spacing = resolve_letter_spacing(style, font_size);
2129 let scaled_font = font.as_scaled(PxScale::from(glyph_font_size));
2130
2131 let mut glyph_x_positions = Vec::new();
2132 let mut char_to_byte = Vec::new();
2133 let mut glyph_layouts = Vec::new();
2134 let mut lines = Vec::new();
2135 let mut current_x = 0.0f32;
2136 let mut line_start = 0;
2137 let mut y = 0.0f32;
2138
2139 let mut iter = text.char_indices().peekable();
2140 while let Some((byte_offset, c)) = iter.next() {
2141 glyph_x_positions.push(current_x);
2142 char_to_byte.push(byte_offset);
2143
2144 if c == '\n' {
2145 lines.push(LineLayout {
2146 start_offset: line_start,
2147 end_offset: byte_offset,
2148 y,
2149 height: line_height,
2150 });
2151 line_start = byte_offset + 1;
2152 y += line_height;
2153 current_x = 0.0;
2154 } else {
2155 let glyph_id = scaled_font.glyph_id(c);
2156 let glyph_width =
2157 weight_synthesis.apply_width(scaled_font.h_advance(glyph_id).max(0.0));
2158 let glyph_end = byte_offset + c.len_utf8();
2159 if glyph_end > byte_offset {
2160 glyph_layouts.push(GlyphLayout {
2161 line_index: lines.len(),
2162 start_offset: byte_offset,
2163 end_offset: glyph_end,
2164 x: current_x,
2165 y,
2166 width: glyph_width,
2167 height: line_height,
2168 });
2169 }
2170 current_x += glyph_width;
2171 if let Some((_, next)) = iter.peek() {
2172 if *next != '\n' {
2173 current_x += letter_spacing;
2174 }
2175 }
2176 }
2177 }
2178
2179 glyph_x_positions.push(current_x);
2180 char_to_byte.push(text.len());
2181
2182 lines.push(LineLayout {
2183 start_offset: line_start,
2184 end_offset: text.len(),
2185 y,
2186 height: line_height,
2187 });
2188
2189 let metrics = measure_text_impl(
2190 text,
2191 style,
2192 font_size,
2193 glyph_font_size,
2194 font,
2195 resolved_style,
2196 resolved_weight,
2197 );
2198 TextLayoutResult::new(
2199 text,
2200 TextLayoutData {
2201 width: metrics.width,
2202 height: metrics.height,
2203 line_height,
2204 glyph_x_positions,
2205 char_to_byte,
2206 lines,
2207 glyph_layouts,
2208 },
2209 )
2210}
2211
2212pub fn rasterize_text_to_image_with_font(
2213 text: &str,
2214 rect: Rect,
2215 style: &TextStyle,
2216 fallback_color: Color,
2217 font_size: f32,
2218 scale: f32,
2219 font: &impl Font,
2220) -> Option<ImageBitmap> {
2221 rasterize_text_to_image_impl(
2222 TextRasterImageRequest {
2223 text,
2224 rect,
2225 style,
2226 fallback_color,
2227 font_size,
2228 scale,
2229 },
2230 RasterFontRef {
2231 font,
2232 ab_glyph_scale_factor: 1.0,
2233 weight: FontWeight::NORMAL,
2234 style: FontStyle::Normal,
2235 },
2236 0,
2237 None,
2238 )
2239}
2240
2241struct TextRasterImageRequest<'a> {
2242 text: &'a str,
2243 rect: Rect,
2244 style: &'a TextStyle,
2245 fallback_color: Color,
2246 font_size: f32,
2247 scale: f32,
2248}
2249
2250fn rasterize_text_to_image_impl(
2251 request: TextRasterImageRequest<'_>,
2252 font_ref: RasterFontRef<'_, impl Font>,
2253 font_cache_key: u64,
2254 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
2255) -> Option<ImageBitmap> {
2256 let TextRasterImageRequest {
2257 text,
2258 rect,
2259 style,
2260 fallback_color,
2261 font_size,
2262 scale,
2263 } = request;
2264
2265 if text.is_empty()
2266 || rect.width <= 0.0
2267 || rect.height <= 0.0
2268 || !font_size.is_finite()
2269 || font_size <= 0.0
2270 || !scale.is_finite()
2271 || scale <= 0.0
2272 {
2273 return None;
2274 }
2275
2276 let width = rect.width.ceil().max(1.0) as u32;
2277 let height = rect.height.ceil().max(1.0) as u32;
2278
2279 let fallback_brush = Brush::solid(fallback_color);
2280 let (brush, brush_alpha_multiplier) = match style.span_style.brush.as_ref() {
2281 Some(brush) => (brush, style.span_style.alpha.unwrap_or(1.0).clamp(0.0, 1.0)),
2282 None => (&fallback_brush, 1.0),
2283 };
2284 let raster_style = match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
2285 TextDrawStyle::Fill => GlyphRasterStyle::Fill,
2286 TextDrawStyle::Stroke { width } => {
2287 if width.is_finite() && width > 0.0 {
2288 GlyphRasterStyle::Stroke {
2289 width_px: width * scale,
2290 }
2291 } else {
2292 GlyphRasterStyle::Fill
2293 }
2294 }
2295 };
2296 let shadow = style
2297 .span_style
2298 .shadow
2299 .filter(|shadow| shadow.color.3 > 0.0);
2300 let static_text_motion = style
2301 .paragraph_style
2302 .text_motion
2303 .unwrap_or(TextMotion::Static)
2304 == TextMotion::Static;
2305
2306 let origin_x = if static_text_motion {
2307 0.0
2308 } else {
2309 rect.x.fract()
2310 };
2311 let origin_y = if static_text_motion {
2312 0.0
2313 } else {
2314 rect.y.fract()
2315 };
2316
2317 let font = font_ref.font;
2318 let font_px_size = font_size * scale * font_ref.ab_glyph_scale_factor;
2319 let letter_spacing = resolve_letter_spacing(style, font_size) * scale;
2320 let weight_synthesis = TextWeightSynthesis::for_style(style, font_ref.weight, font_size, scale);
2321 let style_synthesis = TextStyleSynthesis::for_style(style, font_ref.style, font_size, scale);
2322 let metrics = vertical_metrics(font, font_px_size);
2323 let line_height = (style.resolve_line_height(14.0, font_size * 1.4) * scale).max(1.0);
2324 let first_baseline_y = baseline_y_for_line_box(metrics, line_height);
2325
2326 if let Brush::Solid(color) = brush {
2327 if shadow.is_none() {
2328 let color = color_to_rgba(*color);
2329 let mut rgba = vec![0u8; (width * height * 4) as usize];
2330 visit_text_glyph_masks(
2331 text,
2332 font,
2333 font_cache_key,
2334 font_px_size,
2335 line_height,
2336 first_baseline_y,
2337 origin_x,
2338 origin_y,
2339 letter_spacing,
2340 static_text_motion,
2341 raster_style,
2342 weight_synthesis,
2343 style_synthesis,
2344 glyph_cache.as_deref_mut(),
2345 |mask| {
2346 draw_mask_glyph_solid_u8(
2347 &mut rgba,
2348 width,
2349 height,
2350 mask,
2351 color,
2352 brush_alpha_multiplier,
2353 );
2354 },
2355 );
2356
2357 return ImageBitmap::from_rgba8(width, height, rgba).ok();
2358 }
2359 }
2360
2361 let mut canvas = vec![[0.0f32; 4]; (width * height) as usize];
2362 visit_text_glyph_masks(
2363 text,
2364 font,
2365 font_cache_key,
2366 font_px_size,
2367 line_height,
2368 first_baseline_y,
2369 origin_x,
2370 origin_y,
2371 letter_spacing,
2372 static_text_motion,
2373 raster_style,
2374 weight_synthesis,
2375 style_synthesis,
2376 glyph_cache,
2377 |mask| {
2378 if let Some(shadow) = shadow {
2379 draw_shadow_mask(
2380 &mut canvas,
2381 width,
2382 height,
2383 mask,
2384 shadow,
2385 scale,
2386 static_text_motion,
2387 );
2388 }
2389
2390 draw_mask_glyph(
2391 &mut canvas,
2392 width,
2393 height,
2394 mask,
2395 brush,
2396 brush_alpha_multiplier,
2397 rect,
2398 );
2399 },
2400 );
2401
2402 let mut rgba = vec![0u8; canvas.len() * 4];
2403 for (index, pixel) in canvas.iter().enumerate() {
2404 let base = index * 4;
2405 rgba[base] = (pixel[0].clamp(0.0, 1.0) * 255.0).round() as u8;
2406 rgba[base + 1] = (pixel[1].clamp(0.0, 1.0) * 255.0).round() as u8;
2407 rgba[base + 2] = (pixel[2].clamp(0.0, 1.0) * 255.0).round() as u8;
2408 rgba[base + 3] = (pixel[3].clamp(0.0, 1.0) * 255.0).round() as u8;
2409 }
2410
2411 ImageBitmap::from_rgba8(width, height, rgba).ok()
2412}
2413
2414fn style_can_rasterize_direct_solid(style: &TextStyle) -> bool {
2415 if style
2416 .span_style
2417 .shadow
2418 .is_some_and(|shadow| shadow.color.3 > 0.0)
2419 {
2420 return false;
2421 }
2422 matches!(
2423 style.span_style.brush.as_ref(),
2424 None | Some(Brush::Solid(_))
2425 )
2426}
2427
2428fn style_can_atlas_solid_fill(style: &TextStyle) -> bool {
2429 if style
2430 .span_style
2431 .shadow
2432 .is_some_and(|shadow| shadow.color.3 > 0.0)
2433 {
2434 return false;
2435 }
2436 if !matches!(
2437 style.span_style.brush.as_ref(),
2438 None | Some(Brush::Solid(_))
2439 ) {
2440 return false;
2441 }
2442 match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
2443 TextDrawStyle::Fill => true,
2444 TextDrawStyle::Stroke { width } => !width.is_finite() || width <= 0.0,
2445 }
2446}
2447
2448#[allow(clippy::too_many_arguments)]
2449fn draw_text_segment_solid_to_rgba(
2450 canvas: &mut [u8],
2451 canvas_width: u32,
2452 canvas_height: u32,
2453 text: &str,
2454 local_rect: Rect,
2455 style: &TextStyle,
2456 color: Color,
2457 font_size: f32,
2458 scale: f32,
2459 font: &SoftwareTextFont,
2460 glyph_cache: &mut SoftwareGlyphRasterCache,
2461) -> f32 {
2462 if text.is_empty()
2463 || local_rect.width <= 0.0
2464 || local_rect.height <= 0.0
2465 || !font_size.is_finite()
2466 || font_size <= 0.0
2467 || !scale.is_finite()
2468 || scale <= 0.0
2469 {
2470 return 0.0;
2471 }
2472
2473 let raster_style = match style.span_style.draw_style.unwrap_or(TextDrawStyle::Fill) {
2474 TextDrawStyle::Fill => GlyphRasterStyle::Fill,
2475 TextDrawStyle::Stroke { width } => {
2476 if width.is_finite() && width > 0.0 {
2477 GlyphRasterStyle::Stroke {
2478 width_px: width * scale,
2479 }
2480 } else {
2481 GlyphRasterStyle::Fill
2482 }
2483 }
2484 };
2485 let text_motion_static = style
2486 .paragraph_style
2487 .text_motion
2488 .unwrap_or(TextMotion::Static)
2489 == TextMotion::Static;
2490 let font_px_size = font.ab_glyph_px_size(font_size) * scale;
2491 let letter_spacing = resolve_letter_spacing(style, font_size) * scale;
2492 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, scale);
2493 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, scale);
2494 let metrics = vertical_metrics(&font.font, font_px_size);
2495 let line_height = (style.resolve_line_height(14.0, font_size * 1.4) * scale).max(1.0);
2496 let first_baseline_y = local_rect.y + baseline_y_for_line_box(metrics, line_height);
2497 let origin_x = if text_motion_static {
2498 local_rect.x.round()
2499 } else {
2500 local_rect.x + local_rect.x.fract()
2501 };
2502 let color = color_to_rgba(color);
2503
2504 visit_text_glyph_masks(
2505 text,
2506 &font.font,
2507 font.content_hash(),
2508 font_px_size,
2509 line_height,
2510 first_baseline_y,
2511 origin_x,
2512 0.0,
2513 letter_spacing,
2514 text_motion_static,
2515 raster_style,
2516 weight_synthesis,
2517 style_synthesis,
2518 Some(glyph_cache),
2519 |mask| draw_mask_glyph_solid_u8(canvas, canvas_width, canvas_height, mask, color, 1.0),
2520 )
2521}
2522
2523#[allow(clippy::too_many_arguments)]
2524fn collect_text_segment_solid_atlas_glyphs(
2525 text: &str,
2526 local_rect: Rect,
2527 style: &TextStyle,
2528 color: Color,
2529 font_size: f32,
2530 scale: f32,
2531 font: &SoftwareTextFont,
2532 glyph_cache: &mut SoftwareGlyphRasterCache,
2533 out: &mut Vec<SoftwareGlyphAtlasGlyph>,
2534) -> Option<f32> {
2535 if text.is_empty()
2536 || local_rect.width <= 0.0
2537 || local_rect.height <= 0.0
2538 || !font_size.is_finite()
2539 || font_size <= 0.0
2540 || !scale.is_finite()
2541 || scale <= 0.0
2542 {
2543 return Some(0.0);
2544 }
2545 if !style_can_atlas_solid_fill(style) {
2546 return None;
2547 }
2548
2549 let text_motion_static = style
2550 .paragraph_style
2551 .text_motion
2552 .unwrap_or(TextMotion::Static)
2553 == TextMotion::Static;
2554 if !text_motion_static {
2555 return None;
2556 }
2557
2558 let font_px_size = font.ab_glyph_px_size(font_size) * scale;
2559 let letter_spacing = resolve_letter_spacing(style, font_size) * scale;
2560 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, scale);
2561 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, scale);
2562 let metrics = vertical_metrics(&font.font, font_px_size);
2563 let line_height = (style.resolve_line_height(14.0, font_size * 1.4) * scale).max(1.0);
2564 let first_baseline_y = local_rect.y + baseline_y_for_line_box(metrics, line_height);
2565 let origin_x = local_rect.x.round();
2566 let initial_len = out.len();
2567
2568 let advance = visit_text_glyph_masks_with_key(
2569 text,
2570 &font.font,
2571 font.content_hash(),
2572 font_px_size,
2573 line_height,
2574 first_baseline_y,
2575 origin_x,
2576 0.0,
2577 letter_spacing,
2578 true,
2579 GlyphRasterStyle::Fill,
2580 weight_synthesis,
2581 style_synthesis,
2582 Some(glyph_cache),
2583 |key, mask| {
2584 if mask.width == 0 || mask.height == 0 {
2585 return;
2586 }
2587 out.push(SoftwareGlyphAtlasGlyph {
2588 key,
2589 mask: SoftwareGlyphAtlasMask {
2590 alpha: Arc::clone(&mask.alpha),
2591 width: mask.width,
2592 height: mask.height,
2593 },
2594 x: mask.origin_x,
2595 y: mask.origin_y,
2596 color,
2597 });
2598 },
2599 );
2600
2601 if advance.is_finite() {
2602 Some(advance)
2603 } else {
2604 out.truncate(initial_len);
2605 None
2606 }
2607}
2608
2609#[allow(clippy::too_many_arguments)]
2610fn collect_text_segment_cached_solid_atlas_placements(
2611 text: &str,
2612 local_rect: Rect,
2613 style: &TextStyle,
2614 color: Color,
2615 font_size: f32,
2616 scale: f32,
2617 font: &SoftwareTextFont,
2618 glyph_cache: &mut SoftwareGlyphRasterCache,
2619 out: &mut Vec<SoftwareGlyphAtlasPlacement>,
2620) -> Option<f32> {
2621 if text.is_empty()
2622 || local_rect.width <= 0.0
2623 || local_rect.height <= 0.0
2624 || !font_size.is_finite()
2625 || font_size <= 0.0
2626 || !scale.is_finite()
2627 || scale <= 0.0
2628 {
2629 return Some(0.0);
2630 }
2631 if !style_can_atlas_solid_fill(style) {
2632 return None;
2633 }
2634
2635 let text_motion_static = style
2636 .paragraph_style
2637 .text_motion
2638 .unwrap_or(TextMotion::Static)
2639 == TextMotion::Static;
2640 if !text_motion_static {
2641 return None;
2642 }
2643
2644 let font_px_size = font.ab_glyph_px_size(font_size) * scale;
2645 let letter_spacing = resolve_letter_spacing(style, font_size) * scale;
2646 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, scale);
2647 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, scale);
2648 let metrics = vertical_metrics(&font.font, font_px_size);
2649 let line_height = (style.resolve_line_height(14.0, font_size * 1.4) * scale).max(1.0);
2650 let first_baseline_y = local_rect.y + baseline_y_for_line_box(metrics, line_height);
2651 let origin_x = local_rect.x.round();
2652 let initial_len = out.len();
2653
2654 let advance = visit_cached_text_glyph_atlas_placements(
2655 text,
2656 &font.font,
2657 font.content_hash(),
2658 font_px_size,
2659 line_height,
2660 first_baseline_y,
2661 origin_x,
2662 0.0,
2663 letter_spacing,
2664 GlyphRasterStyle::Fill,
2665 weight_synthesis,
2666 style_synthesis,
2667 glyph_cache,
2668 |placement| {
2669 if placement.width == 0 || placement.height == 0 {
2670 return;
2671 }
2672 out.push(SoftwareGlyphAtlasPlacement { color, ..placement });
2673 },
2674 );
2675
2676 if advance.is_finite() {
2677 Some(advance)
2678 } else {
2679 out.truncate(initial_len);
2680 None
2681 }
2682}
2683
2684#[allow(clippy::too_many_arguments)]
2685fn collect_text_segment_solid_atlas_run(
2686 text: &str,
2687 local_rect: Rect,
2688 style: &TextStyle,
2689 color: Color,
2690 font_size: f32,
2691 scale: f32,
2692 font: &SoftwareTextFont,
2693 glyph_cache: &mut SoftwareGlyphRasterCache,
2694 out: &mut Vec<SoftwareGlyphAtlasRunGlyph>,
2695) -> Option<f32> {
2696 if text.is_empty()
2697 || local_rect.width <= 0.0
2698 || local_rect.height <= 0.0
2699 || !font_size.is_finite()
2700 || font_size <= 0.0
2701 || !scale.is_finite()
2702 || scale <= 0.0
2703 {
2704 return Some(0.0);
2705 }
2706 if !style_can_atlas_solid_fill(style) {
2707 return None;
2708 }
2709
2710 let text_motion_static = style
2711 .paragraph_style
2712 .text_motion
2713 .unwrap_or(TextMotion::Static)
2714 == TextMotion::Static;
2715 if !text_motion_static {
2716 return None;
2717 }
2718
2719 let font_px_size = font.ab_glyph_px_size(font_size) * scale;
2720 let letter_spacing = resolve_letter_spacing(style, font_size) * scale;
2721 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, scale);
2722 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, scale);
2723 let metrics = vertical_metrics(&font.font, font_px_size);
2724 let line_height = (style.resolve_line_height(14.0, font_size * 1.4) * scale).max(1.0);
2725 let first_baseline_y = local_rect.y + baseline_y_for_line_box(metrics, line_height);
2726 let origin_x = local_rect.x.round();
2727 let initial_len = out.len();
2728
2729 let advance = visit_text_glyph_atlas_run(
2730 text,
2731 &font.font,
2732 font.content_hash(),
2733 font_px_size,
2734 line_height,
2735 first_baseline_y,
2736 origin_x,
2737 0.0,
2738 letter_spacing,
2739 GlyphRasterStyle::Fill,
2740 weight_synthesis,
2741 style_synthesis,
2742 glyph_cache,
2743 |run_glyph| {
2744 let run_glyph = match run_glyph {
2745 SoftwareGlyphAtlasRunGlyph::Cached(mut placement) => {
2746 if placement.width == 0 || placement.height == 0 {
2747 return;
2748 }
2749 placement.color = color;
2750 SoftwareGlyphAtlasRunGlyph::Cached(placement)
2751 }
2752 SoftwareGlyphAtlasRunGlyph::New(mut glyph) => {
2753 if glyph.mask.width == 0 || glyph.mask.height == 0 {
2754 return;
2755 }
2756 glyph.color = color;
2757 SoftwareGlyphAtlasRunGlyph::New(glyph)
2758 }
2759 };
2760 out.push(run_glyph);
2761 },
2762 );
2763
2764 if advance.is_finite() {
2765 Some(advance)
2766 } else {
2767 out.truncate(initial_len);
2768 None
2769 }
2770}
2771
2772fn resolve_font_size(style: &TextStyle) -> f32 {
2773 style.resolve_font_size(14.0)
2774}
2775
2776fn baseline_y_for_line_box(
2777 metrics: crate::font_layout::FontVerticalMetrics,
2778 line_height: f32,
2779) -> f32 {
2780 metrics.ascent + (line_height - metrics.natural_line_height) * 0.5
2781}
2782
2783fn resolve_line_height(style: &TextStyle, font_size: f32) -> f32 {
2784 style.resolve_line_height(14.0, font_size)
2785}
2786
2787fn line_height_for_render_style(style: &TextStyle, font_size: f32) -> f32 {
2788 resolve_line_height(style, font_size * 1.4).max(1.0)
2789}
2790
2791fn resolve_letter_spacing(style: &TextStyle, font_size: f32) -> f32 {
2792 let _ = font_size;
2793 style.resolve_letter_spacing(14.0)
2794}
2795
2796fn fallback_char_width(font_size: f32) -> f32 {
2797 font_size.max(1.0) * 0.55
2798}
2799
2800fn fallback_line_height(style: &TextStyle, font_size: f32) -> f32 {
2801 resolve_line_height(style, font_size.max(1.0) * 1.2)
2802}
2803
2804fn fallback_line_heights(text: &str, style: &TextStyle, font_size: f32) -> Vec<f32> {
2805 let line_count = text.split('\n').count().max(1);
2806 vec![fallback_line_height(style, font_size); line_count]
2807}
2808
2809fn fallback_text_metrics(text: &str, style: &TextStyle, font_size: f32) -> TextMetrics {
2810 let line_height = fallback_line_height(style, font_size);
2811 let char_width = fallback_char_width(font_size);
2812 let letter_spacing = resolve_letter_spacing(style, font_size);
2813 let mut line_count = 0usize;
2814 let mut max_width = 0.0f32;
2815
2816 for line in text.split('\n') {
2817 line_count += 1;
2818 let char_count = line.chars().count();
2819 let spacing = char_count.saturating_sub(1) as f32 * letter_spacing;
2820 max_width = max_width.max(char_count as f32 * char_width + spacing);
2821 }
2822
2823 let line_count = line_count.max(1);
2824 TextMetrics {
2825 width: max_width,
2826 height: line_count as f32 * line_height,
2827 line_height,
2828 line_count,
2829 }
2830}
2831
2832fn fallback_cursor_x_for_offset(text: &str, style: &TextStyle, offset: usize) -> f32 {
2833 let font_size = resolve_font_size(style);
2834 let clamped = clamp_to_char_boundary(text, offset.min(text.len()));
2835 let line_start = text[..clamped].rfind('\n').map_or(0, |index| index + 1);
2836 let char_count = text[line_start..clamped].chars().count();
2837 let spacing = char_count.saturating_sub(1) as f32 * resolve_letter_spacing(style, font_size);
2838 char_count as f32 * fallback_char_width(font_size) + spacing
2839}
2840
2841fn fallback_text_offset_for_position(text: &str, style: &TextStyle, x: f32, y: f32) -> usize {
2842 if text.is_empty() {
2843 return 0;
2844 }
2845
2846 let font_size = resolve_font_size(style);
2847 let line_height = fallback_line_height(style, font_size);
2848 let line_index = (y / line_height).floor().max(0.0) as usize;
2849 let lines: Vec<&str> = text.split('\n').collect();
2850 let target_line = line_index.min(lines.len().saturating_sub(1));
2851
2852 let mut line_start_byte = 0;
2853 for line in lines.iter().take(target_line) {
2854 line_start_byte += line.len() + 1;
2855 }
2856
2857 let line_text = lines.get(target_line).copied().unwrap_or("");
2858 if line_text.is_empty() {
2859 return line_start_byte;
2860 }
2861
2862 let advance =
2863 (fallback_char_width(font_size) + resolve_letter_spacing(style, font_size)).max(1.0);
2864 let target_char = (x / advance).round().max(0.0) as usize;
2865 line_start_byte + byte_offset_for_char_index(line_text, target_char)
2866}
2867
2868fn fallback_layout_text(text: &str, style: &TextStyle) -> TextLayoutResult {
2869 let font_size = resolve_font_size(style);
2870 let line_height = fallback_line_height(style, font_size);
2871 let char_width = fallback_char_width(font_size);
2872 let letter_spacing = resolve_letter_spacing(style, font_size);
2873
2874 let mut glyph_x_positions = Vec::new();
2875 let mut char_to_byte = Vec::new();
2876 let mut glyph_layouts = Vec::new();
2877 let mut lines = Vec::new();
2878 let mut current_x = 0.0f32;
2879 let mut line_start = 0;
2880 let mut y = 0.0f32;
2881
2882 let mut iter = text.char_indices().peekable();
2883 while let Some((byte_offset, ch)) = iter.next() {
2884 glyph_x_positions.push(current_x);
2885 char_to_byte.push(byte_offset);
2886
2887 if ch == '\n' {
2888 lines.push(LineLayout {
2889 start_offset: line_start,
2890 end_offset: byte_offset,
2891 y,
2892 height: line_height,
2893 });
2894 line_start = byte_offset + 1;
2895 y += line_height;
2896 current_x = 0.0;
2897 } else {
2898 glyph_layouts.push(GlyphLayout {
2899 line_index: lines.len(),
2900 start_offset: byte_offset,
2901 end_offset: byte_offset + ch.len_utf8(),
2902 x: current_x,
2903 y,
2904 width: char_width,
2905 height: line_height,
2906 });
2907 current_x += char_width;
2908 if let Some((_, next)) = iter.peek() {
2909 if *next != '\n' {
2910 current_x += letter_spacing;
2911 }
2912 }
2913 }
2914 }
2915
2916 glyph_x_positions.push(current_x);
2917 char_to_byte.push(text.len());
2918 lines.push(LineLayout {
2919 start_offset: line_start,
2920 end_offset: text.len(),
2921 y,
2922 height: line_height,
2923 });
2924
2925 let metrics = fallback_text_metrics(text, style, font_size);
2926 TextLayoutResult::new(
2927 text,
2928 TextLayoutData {
2929 width: metrics.width,
2930 height: metrics.height,
2931 line_height,
2932 glyph_x_positions,
2933 char_to_byte,
2934 glyph_layouts,
2935 lines,
2936 },
2937 )
2938}
2939
2940fn style_allows_prefix_widths(style: &TextStyle) -> bool {
2941 !matches!(
2942 style
2943 .paragraph_style
2944 .platform_style
2945 .and_then(|platform| platform.shaping),
2946 Some(TextShaping::Advanced)
2947 )
2948}
2949
2950fn cached_line_advance_width(
2951 font: &SoftwareTextFont,
2952 text: &str,
2953 glyph_font_size: f32,
2954 glyph_metrics: &mut SoftwareTextGlyphMetricsCache,
2955) -> f32 {
2956 let scaled_font = font.font.as_scaled(PxScale::from(glyph_font_size));
2957 let mut width = 0.0f32;
2958 let mut previous = None;
2959
2960 for ch in text.chars() {
2961 let metrics = glyph_metrics.glyph_metrics(font, glyph_font_size, &scaled_font, ch);
2962 if let Some(previous_id) = previous {
2963 width += glyph_metrics.kern(
2964 font,
2965 glyph_font_size,
2966 &scaled_font,
2967 previous_id,
2968 metrics.glyph_id,
2969 );
2970 }
2971 width += metrics.advance;
2972 previous = Some(metrics.glyph_id);
2973 }
2974
2975 width.max(0.0)
2976}
2977
2978fn annotated_line_prefix_widths_with_font_set_cached(
2979 text: &AnnotatedString,
2980 line_range: std::ops::Range<usize>,
2981 style: &TextStyle,
2982 fonts: &SoftwareTextFontSet,
2983 cache: &mut SoftwareTextMetricsCache,
2984) -> Option<TextLinePrefixWidths> {
2985 let mut boundaries = text.span_boundaries();
2986 boundaries.push(line_range.start);
2987 boundaries.push(line_range.end);
2988 boundaries.sort_unstable();
2989 boundaries.dedup();
2990 boundaries.retain(|offset| {
2991 *offset >= line_range.start
2992 && *offset <= line_range.end
2993 && text.text.is_char_boundary(*offset)
2994 });
2995
2996 let char_count = text.text[line_range.clone()].chars().count();
2997 let mut prefix_widths = Vec::with_capacity(char_count + 1);
2998 let mut separator_before = Vec::with_capacity(char_count);
2999 let non_empty_overhang = {
3000 let mut sink = PrefixWidthSegmentSink {
3001 prefix_widths: &mut prefix_widths,
3002 separator_before: &mut separator_before,
3003 width: 0.0,
3004 non_empty_overhang: 0.0,
3005 };
3006 sink.prefix_widths.push(sink.width);
3007
3008 for range in boundaries.windows(2) {
3009 let start = range[0];
3010 let end = range[1];
3011 if start >= end {
3012 continue;
3013 }
3014 let segment = &text.text[start..end];
3015 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
3016 append_prefix_width_segment_cached(segment, &segment_style, fonts, cache, &mut sink);
3017 }
3018
3019 sink.non_empty_overhang
3020 };
3021
3022 TextLinePrefixWidths::from_parts(prefix_widths, separator_before, non_empty_overhang)
3023}
3024
3025struct PrefixWidthSegmentSink<'a> {
3026 prefix_widths: &'a mut Vec<f32>,
3027 separator_before: &'a mut Vec<f32>,
3028 width: f32,
3029 non_empty_overhang: f32,
3030}
3031
3032fn append_prefix_width_segment_cached(
3033 segment: &str,
3034 style: &TextStyle,
3035 fonts: &SoftwareTextFontSet,
3036 cache: &mut SoftwareTextMetricsCache,
3037 sink: &mut PrefixWidthSegmentSink<'_>,
3038) {
3039 if segment.is_empty() {
3040 return;
3041 }
3042
3043 let font_size = resolve_font_size(style);
3044 if let Some(font) = fonts.resolve(style) {
3045 append_font_prefix_width_segment_cached(segment, style, font_size, font, cache, sink);
3046 } else {
3047 append_fallback_prefix_width_segment(segment, style, font_size, sink);
3048 }
3049}
3050
3051fn append_font_prefix_width_segment_cached(
3052 segment: &str,
3053 style: &TextStyle,
3054 font_size: f32,
3055 font: &SoftwareTextFont,
3056 cache: &mut SoftwareTextMetricsCache,
3057 sink: &mut PrefixWidthSegmentSink<'_>,
3058) {
3059 let glyph_font_size = font.ab_glyph_px_size(font_size);
3060 let scaled_font = font.font.as_scaled(PxScale::from(glyph_font_size));
3061 let letter_spacing = resolve_letter_spacing(style, font_size);
3062 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, 1.0);
3063 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0);
3064 sink.non_empty_overhang = sink
3065 .non_empty_overhang
3066 .max(style_synthesis.visual_overhang_px());
3067
3068 let mut previous = None;
3069
3070 for (index, ch) in segment.chars().enumerate() {
3071 let metrics = cache
3072 .glyph_metrics
3073 .glyph_metrics(font, glyph_font_size, &scaled_font, ch);
3074 let separator = if index == 0 {
3075 0.0
3076 } else {
3077 previous
3078 .map(|previous_id| {
3079 weight_synthesis.apply_width(cache.glyph_metrics.kern(
3080 font,
3081 glyph_font_size,
3082 &scaled_font,
3083 previous_id,
3084 metrics.glyph_id,
3085 ))
3086 })
3087 .unwrap_or(0.0)
3088 + letter_spacing
3089 };
3090 sink.separator_before.push(separator);
3091 sink.width += separator + weight_synthesis.apply_width(metrics.advance);
3092 sink.prefix_widths.push(sink.width.max(0.0));
3093 previous = Some(metrics.glyph_id);
3094 }
3095}
3096
3097fn append_fallback_prefix_width_segment(
3098 segment: &str,
3099 style: &TextStyle,
3100 font_size: f32,
3101 sink: &mut PrefixWidthSegmentSink<'_>,
3102) {
3103 let char_width = fallback_char_width(font_size);
3104 let letter_spacing = resolve_letter_spacing(style, font_size);
3105 for (index, _) in segment.chars().enumerate() {
3106 let separator = if index == 0 { 0.0 } else { letter_spacing };
3107 sink.separator_before.push(separator);
3108 sink.width += separator + char_width;
3109 sink.prefix_widths.push(sink.width.max(0.0));
3110 }
3111}
3112
3113fn byte_offset_for_char_index(text: &str, char_index: usize) -> usize {
3114 text.char_indices()
3115 .map(|(index, _)| index)
3116 .nth(char_index)
3117 .unwrap_or(text.len())
3118}
3119
3120fn measure_text_impl(
3121 text: &str,
3122 style: &TextStyle,
3123 font_size: f32,
3124 glyph_font_size: f32,
3125 font: &impl Font,
3126 resolved_style: FontStyle,
3127 resolved_weight: FontWeight,
3128) -> TextMetrics {
3129 let line_height = resolve_line_height(style, font_size * 1.4);
3130 let letter_spacing = resolve_letter_spacing(style, font_size);
3131 let weight_synthesis = TextWeightSynthesis::for_style(style, resolved_weight, font_size, 1.0);
3132 let style_synthesis = TextStyleSynthesis::for_style(style, resolved_style, font_size, 1.0);
3133
3134 let lines: Vec<&str> = text.split('\n').collect();
3135 let line_count = lines.len().max(1);
3136
3137 let mut max_width: f32 = 0.0;
3138 for line in &lines {
3139 let line_width = line_advance_width(font, line, glyph_font_size);
3140 let char_spacing = (line.chars().count().saturating_sub(1) as f32) * letter_spacing;
3141 let line_width = (weight_synthesis.apply_width(line_width) + char_spacing).max(0.0);
3142 let line_width = if line.is_empty() {
3143 line_width
3144 } else {
3145 line_width + style_synthesis.visual_overhang_px()
3146 };
3147 max_width = max_width.max(line_width);
3148 }
3149
3150 TextMetrics {
3151 width: max_width,
3152 height: line_count as f32 * line_height,
3153 line_height,
3154 line_count,
3155 }
3156}
3157
3158fn measure_text_impl_cached(
3159 text: &str,
3160 style: &TextStyle,
3161 font_size: f32,
3162 font: &SoftwareTextFont,
3163 cache: &mut SoftwareTextMetricsCache,
3164) -> TextMetrics {
3165 let line_height = resolve_line_height(style, font_size * 1.4);
3166 let letter_spacing = resolve_letter_spacing(style, font_size);
3167 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, 1.0);
3168 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0);
3169 let glyph_font_size = font.ab_glyph_px_size(font_size);
3170
3171 let lines: Vec<&str> = text.split('\n').collect();
3172 let line_count = lines.len().max(1);
3173
3174 let mut max_width: f32 = 0.0;
3175 for line in &lines {
3176 let line_width =
3177 cached_line_advance_width(font, line, glyph_font_size, &mut cache.glyph_metrics);
3178 let char_spacing = (line.chars().count().saturating_sub(1) as f32) * letter_spacing;
3179 let line_width = (weight_synthesis.apply_width(line_width) + char_spacing).max(0.0);
3180 let line_width = if line.is_empty() {
3181 line_width
3182 } else {
3183 line_width + style_synthesis.visual_overhang_px()
3184 };
3185 max_width = max_width.max(line_width);
3186 }
3187
3188 TextMetrics {
3189 width: max_width,
3190 height: line_count as f32 * line_height,
3191 line_height,
3192 line_count,
3193 }
3194}
3195
3196fn measure_annotated_text_with_resolver(
3197 text: &AnnotatedString,
3198 style: &TextStyle,
3199 font_size: f32,
3200 fonts: &SoftwareTextFontSet,
3201 mut cache: Option<&mut SoftwareTextMetricsCache>,
3202) -> TextMetrics {
3203 let Some(base_font) = fonts.resolve(style) else {
3204 return fallback_text_metrics(text.text.as_str(), style, font_size);
3205 };
3206 let base_line_height = line_height_for_style(style, font_size, &base_font.font);
3207 let mut boundaries = text.span_boundaries();
3208 for (offset, ch) in text.text.char_indices() {
3209 if ch == '\n' {
3210 boundaries.push(offset);
3211 boundaries.push(offset + ch.len_utf8());
3212 }
3213 }
3214 boundaries.sort_unstable();
3215 boundaries.dedup();
3216 boundaries.retain(|offset| *offset <= text.text.len() && text.text.is_char_boundary(*offset));
3217
3218 let mut line_count = 1usize;
3219 let mut max_width = 0.0f32;
3220 let mut current_line_width = 0.0f32;
3221
3222 for range in boundaries.windows(2) {
3223 let start = range[0];
3224 let end = range[1];
3225 if start == end {
3226 continue;
3227 }
3228 let segment = &text.text[start..end];
3229 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
3230 let segment_font_size = resolve_font_size(&segment_style);
3231 let Some(segment_font) = fonts.resolve(&segment_style) else {
3232 let mut remaining = segment;
3233 loop {
3234 if let Some(newline_offset) = remaining.find('\n') {
3235 let before_newline = &remaining[..newline_offset];
3236 if !before_newline.is_empty() {
3237 current_line_width += fallback_text_metrics(
3238 before_newline,
3239 &segment_style,
3240 segment_font_size,
3241 )
3242 .width;
3243 }
3244 max_width = max_width.max(current_line_width);
3245 current_line_width = 0.0;
3246 line_count += 1;
3247 remaining = &remaining[newline_offset + 1..];
3248 if remaining.is_empty() {
3249 break;
3250 }
3251 } else {
3252 if !remaining.is_empty() {
3253 current_line_width +=
3254 fallback_text_metrics(remaining, &segment_style, segment_font_size)
3255 .width;
3256 }
3257 break;
3258 }
3259 }
3260 continue;
3261 };
3262
3263 let mut remaining = segment;
3264 loop {
3265 if let Some(newline_offset) = remaining.find('\n') {
3266 let before_newline = &remaining[..newline_offset];
3267 if !before_newline.is_empty() {
3268 let metrics = if let Some(cache) = cache.as_deref_mut() {
3269 measure_text_with_font_cached(
3270 before_newline,
3271 &segment_style,
3272 segment_font_size,
3273 segment_font,
3274 cache,
3275 )
3276 } else {
3277 measure_text_with_font(
3278 before_newline,
3279 &segment_style,
3280 segment_font_size,
3281 segment_font,
3282 )
3283 };
3284 current_line_width += metrics.width;
3285 }
3286 max_width = max_width.max(current_line_width);
3287 current_line_width = 0.0;
3288 line_count += 1;
3289 remaining = &remaining[newline_offset + 1..];
3290 if remaining.is_empty() {
3291 break;
3292 }
3293 } else {
3294 if !remaining.is_empty() {
3295 let metrics = if let Some(cache) = cache.as_deref_mut() {
3296 measure_text_with_font_cached(
3297 remaining,
3298 &segment_style,
3299 segment_font_size,
3300 segment_font,
3301 cache,
3302 )
3303 } else {
3304 measure_text_with_font(
3305 remaining,
3306 &segment_style,
3307 segment_font_size,
3308 segment_font,
3309 )
3310 };
3311 current_line_width += metrics.width;
3312 }
3313 break;
3314 }
3315 }
3316 }
3317
3318 max_width = max_width.max(current_line_width);
3319
3320 let line_heights = annotated_line_heights_with_resolver(text, style, font_size, fonts);
3321 let total_height = line_heights.iter().sum();
3322 let max_line_height = line_heights.into_iter().fold(base_line_height, f32::max);
3323
3324 TextMetrics {
3325 width: max_width,
3326 height: total_height,
3327 line_height: max_line_height,
3328 line_count,
3329 }
3330}
3331
3332fn annotated_line_heights_with_resolver(
3333 text: &AnnotatedString,
3334 style: &TextStyle,
3335 font_size: f32,
3336 fonts: &SoftwareTextFontSet,
3337) -> Vec<f32> {
3338 let Some(base_font) = fonts.resolve(style) else {
3339 return fallback_line_heights(text.text.as_str(), style, font_size);
3340 };
3341 let base_line_height = line_height_for_style(style, font_size, &base_font.font);
3342 let mut line_heights = vec![base_line_height];
3343 let mut boundaries = text.span_boundaries();
3344 for (offset, ch) in text.text.char_indices() {
3345 if ch == '\n' {
3346 boundaries.push(offset);
3347 boundaries.push(offset + ch.len_utf8());
3348 }
3349 }
3350 boundaries.sort_unstable();
3351 boundaries.dedup();
3352 boundaries.retain(|offset| *offset <= text.text.len() && text.text.is_char_boundary(*offset));
3353
3354 let mut line_index = 0usize;
3355 for range in boundaries.windows(2) {
3356 let start = range[0];
3357 let end = range[1];
3358 if start == end {
3359 continue;
3360 }
3361 let segment = &text.text[start..end];
3362 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
3363 let segment_font_size = resolve_font_size(&segment_style);
3364 let segment_line_height = if let Some(segment_font) = fonts.resolve(&segment_style) {
3365 line_height_for_style(&segment_style, segment_font_size, &segment_font.font)
3366 } else {
3367 fallback_line_height(&segment_style, segment_font_size)
3368 };
3369 for ch in segment.chars() {
3370 line_heights[line_index] = line_heights[line_index].max(segment_line_height);
3371 if ch == '\n' {
3372 line_index += 1;
3373 if line_heights.len() <= line_index {
3374 line_heights.push(base_line_height);
3375 }
3376 }
3377 }
3378 }
3379
3380 line_heights
3381}
3382
3383fn max_line_height_for_annotated_text_with_resolver(
3384 text: &AnnotatedString,
3385 style: &TextStyle,
3386 font_size: f32,
3387 fonts: &SoftwareTextFontSet,
3388) -> f32 {
3389 let base_line_height = fonts
3390 .resolve(style)
3391 .map(|font| line_height_for_style(style, font_size, &font.font))
3392 .unwrap_or_else(|| fallback_line_height(style, font_size));
3393 if text.span_styles.is_empty() {
3394 return base_line_height;
3395 }
3396
3397 let mut max_line_height = base_line_height;
3398 for range in text.span_boundaries().windows(2) {
3399 let start = range[0];
3400 let end = range[1];
3401 if start == end {
3402 continue;
3403 }
3404 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
3405 let segment_font_size = resolve_font_size(&segment_style);
3406 let segment_line_height = fonts
3407 .resolve(&segment_style)
3408 .map(|font| line_height_for_style(&segment_style, segment_font_size, &font.font))
3409 .unwrap_or_else(|| fallback_line_height(&segment_style, segment_font_size));
3410 max_line_height = max_line_height.max(segment_line_height);
3411 }
3412 max_line_height
3413}
3414
3415fn effective_style_for_range(
3416 span_styles: &[RangeStyle<SpanStyle>],
3417 style: &TextStyle,
3418 start: usize,
3419 end: usize,
3420) -> TextStyle {
3421 let mut effective = style.clone();
3422 for span in span_styles {
3423 if span.range.start < end && span.range.end > start {
3424 effective.span_style = effective.span_style.merge(&span.item);
3425 }
3426 }
3427 effective
3428}
3429
3430fn line_height_for_style(style: &TextStyle, font_size: f32, font: &impl Font) -> f32 {
3431 let _ = font;
3432 resolve_line_height(style, font_size * 1.4)
3433}
3434
3435fn clamp_to_char_boundary(text: &str, mut offset: usize) -> usize {
3436 offset = offset.min(text.len());
3437 while offset > 0 && !text.is_char_boundary(offset) {
3438 offset -= 1;
3439 }
3440 offset
3441}
3442
3443fn align_glyph_for_text_motion(glyph: Glyph, static_text_motion: bool) -> Glyph {
3444 align_glyph_to_pixel_grid(glyph, static_text_motion)
3445}
3446
3447fn static_glyph_pixel_origin(glyph: &Glyph) -> (i32, i32) {
3448 (
3449 glyph.position.x.round() as i32,
3450 glyph.position.y.round() as i32,
3451 )
3452}
3453
3454fn glyph_mask_cache_key(
3455 font_hash: u64,
3456 glyph: &Glyph,
3457 raster_style: GlyphRasterStyle,
3458 weight_synthesis: TextWeightSynthesis,
3459 style_synthesis: TextStyleSynthesis,
3460) -> GlyphMaskCacheKey {
3461 GlyphMaskCacheKey {
3462 font_hash,
3463 glyph_id: u32::from(glyph.id.0),
3464 scale_x_bits: glyph.scale.x.to_bits(),
3465 scale_y_bits: glyph.scale.y.to_bits(),
3466 raster_style: GlyphRasterStyleKey::from_style(raster_style),
3467 embolden_px_bits: weight_synthesis.embolden_px.to_bits(),
3468 slant_bits: style_synthesis.slant.to_bits(),
3469 }
3470}
3471
3472fn glyph_atlas_key_from_mask_key(key: GlyphMaskCacheKey) -> Option<SoftwareGlyphAtlasKey> {
3473 if !matches!(key.raster_style, GlyphRasterStyleKey::Fill) {
3474 return None;
3475 }
3476 Some(SoftwareGlyphAtlasKey {
3477 font_hash: key.font_hash,
3478 glyph_id: key.glyph_id,
3479 scale_x_bits: key.scale_x_bits,
3480 scale_y_bits: key.scale_y_bits,
3481 embolden_px_bits: key.embolden_px_bits,
3482 slant_bits: key.slant_bits,
3483 })
3484}
3485
3486fn build_complete_glyph_mask(
3487 font: &impl Font,
3488 glyph: &Glyph,
3489 raster_style: GlyphRasterStyle,
3490 weight_synthesis: TextWeightSynthesis,
3491 style_synthesis: TextStyleSynthesis,
3492) -> Option<GlyphMask> {
3493 let (outlined, bounds) = outline_glyph_with_bounds(font, glyph)?;
3494 let mask = build_glyph_mask(font, glyph, &outlined, bounds, raster_style)?;
3495 let mask = synthesize_glyph_weight(mask, weight_synthesis);
3496 Some(synthesize_glyph_style(mask, style_synthesis))
3497}
3498
3499fn cached_static_glyph_mask_with_key(
3500 cache: &mut SoftwareGlyphRasterCache,
3501 font_hash: u64,
3502 font: &impl Font,
3503 glyph: &Glyph,
3504 raster_style: GlyphRasterStyle,
3505 weight_synthesis: TextWeightSynthesis,
3506 style_synthesis: TextStyleSynthesis,
3507) -> Option<(GlyphMaskCacheKey, GlyphMask)> {
3508 let key = glyph_mask_cache_key(
3509 font_hash,
3510 glyph,
3511 raster_style,
3512 weight_synthesis,
3513 style_synthesis,
3514 );
3515 if let Some(mask) = cache.get(&key, glyph) {
3516 return Some((key, mask));
3517 }
3518 let mask =
3519 build_complete_glyph_mask(font, glyph, raster_style, weight_synthesis, style_synthesis)?;
3520 Some((key, cache.put(key, glyph, mask)))
3521}
3522
3523fn cached_static_glyph_mask(
3524 cache: &mut SoftwareGlyphRasterCache,
3525 font_hash: u64,
3526 font: &impl Font,
3527 glyph: &Glyph,
3528 raster_style: GlyphRasterStyle,
3529 weight_synthesis: TextWeightSynthesis,
3530 style_synthesis: TextStyleSynthesis,
3531) -> Option<GlyphMask> {
3532 cached_static_glyph_mask_with_key(
3533 cache,
3534 font_hash,
3535 font,
3536 glyph,
3537 raster_style,
3538 weight_synthesis,
3539 style_synthesis,
3540 )
3541 .map(|(_, mask)| mask)
3542}
3543
3544#[allow(clippy::too_many_arguments)]
3545fn visit_text_glyph_masks(
3546 text: &str,
3547 font: &impl Font,
3548 font_hash: u64,
3549 font_px_size: f32,
3550 line_height: f32,
3551 first_baseline_y: f32,
3552 origin_x: f32,
3553 origin_y: f32,
3554 letter_spacing: f32,
3555 static_text_motion: bool,
3556 raster_style: GlyphRasterStyle,
3557 weight_synthesis: TextWeightSynthesis,
3558 style_synthesis: TextStyleSynthesis,
3559 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
3560 mut visit: impl FnMut(&GlyphMask),
3561) -> f32 {
3562 let scale = PxScale::from(font_px_size);
3563 let scaled_font = font.as_scaled(scale);
3564 let mut max_advance = 0.0f32;
3565 for (line_idx, line) in text.split('\n').enumerate() {
3566 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
3567 let mut caret_x = origin_x;
3568 let mut previous = None;
3569 for ch in line.chars() {
3570 let glyph_id = scaled_font.glyph_id(ch);
3571 if let Some(previous_id) = previous {
3572 caret_x += scaled_font.kern(previous_id, glyph_id) + letter_spacing;
3573 }
3574 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
3575 caret_x += scaled_font.h_advance(glyph_id);
3576 previous = Some(glyph_id);
3577 let glyph = align_glyph_for_text_motion(glyph, static_text_motion);
3578 let Some(mask) = (if static_text_motion {
3579 glyph_cache.as_deref_mut().and_then(|cache| {
3580 cached_static_glyph_mask(
3581 cache,
3582 font_hash,
3583 font,
3584 &glyph,
3585 raster_style,
3586 weight_synthesis,
3587 style_synthesis,
3588 )
3589 })
3590 } else {
3591 None
3592 })
3593 .or_else(|| {
3594 build_complete_glyph_mask(
3595 font,
3596 &glyph,
3597 raster_style,
3598 weight_synthesis,
3599 style_synthesis,
3600 )
3601 }) else {
3602 continue;
3603 };
3604 visit(&mask);
3605 }
3606 max_advance = max_advance.max((caret_x - origin_x).max(0.0));
3607 }
3608 max_advance
3609}
3610
3611#[allow(clippy::too_many_arguments)]
3612fn visit_text_glyph_masks_with_key(
3613 text: &str,
3614 font: &impl Font,
3615 font_hash: u64,
3616 font_px_size: f32,
3617 line_height: f32,
3618 first_baseline_y: f32,
3619 origin_x: f32,
3620 origin_y: f32,
3621 letter_spacing: f32,
3622 static_text_motion: bool,
3623 raster_style: GlyphRasterStyle,
3624 weight_synthesis: TextWeightSynthesis,
3625 style_synthesis: TextStyleSynthesis,
3626 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
3627 mut visit: impl FnMut(SoftwareGlyphAtlasKey, &GlyphMask),
3628) -> f32 {
3629 if !static_text_motion {
3630 return 0.0;
3631 }
3632
3633 let scale = PxScale::from(font_px_size);
3634 let scaled_font = font.as_scaled(scale);
3635 let mut max_advance = 0.0f32;
3636 for (line_idx, line) in text.split('\n').enumerate() {
3637 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
3638 let mut caret_x = origin_x;
3639 let mut previous = None;
3640 for ch in line.chars() {
3641 let glyph_id = scaled_font.glyph_id(ch);
3642 if let Some(previous_id) = previous {
3643 caret_x += scaled_font.kern(previous_id, glyph_id) + letter_spacing;
3644 }
3645 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
3646 caret_x += scaled_font.h_advance(glyph_id);
3647 previous = Some(glyph_id);
3648 let glyph = align_glyph_for_text_motion(glyph, true);
3649 let Some((cache_key, mask)) = glyph_cache.as_deref_mut().and_then(|cache| {
3650 cached_static_glyph_mask_with_key(
3651 cache,
3652 font_hash,
3653 font,
3654 &glyph,
3655 raster_style,
3656 weight_synthesis,
3657 style_synthesis,
3658 )
3659 }) else {
3660 continue;
3661 };
3662 let Some(atlas_key) = glyph_atlas_key_from_mask_key(cache_key) else {
3663 continue;
3664 };
3665 visit(atlas_key, &mask);
3666 }
3667 max_advance = max_advance.max((caret_x - origin_x).max(0.0));
3668 }
3669 max_advance
3670}
3671
3672#[allow(clippy::too_many_arguments)]
3673fn visit_cached_text_glyph_atlas_placements(
3674 text: &str,
3675 font: &impl Font,
3676 font_hash: u64,
3677 font_px_size: f32,
3678 line_height: f32,
3679 first_baseline_y: f32,
3680 origin_x: f32,
3681 origin_y: f32,
3682 letter_spacing: f32,
3683 raster_style: GlyphRasterStyle,
3684 weight_synthesis: TextWeightSynthesis,
3685 style_synthesis: TextStyleSynthesis,
3686 glyph_cache: &mut SoftwareGlyphRasterCache,
3687 mut visit: impl FnMut(SoftwareGlyphAtlasPlacement),
3688) -> f32 {
3689 let scale = PxScale::from(font_px_size);
3690 let scaled_font = font.as_scaled(scale);
3691 let mut max_advance = 0.0f32;
3692 for (line_idx, line) in text.split('\n').enumerate() {
3693 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
3694 let mut caret_x = origin_x;
3695 let mut previous = None;
3696 for ch in line.chars() {
3697 let glyph_id = scaled_font.glyph_id(ch);
3698 if let Some(previous_id) = previous {
3699 caret_x += scaled_font.kern(previous_id, glyph_id) + letter_spacing;
3700 }
3701 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
3702 caret_x += scaled_font.h_advance(glyph_id);
3703 previous = Some(glyph_id);
3704 let glyph = align_glyph_for_text_motion(glyph, true);
3705 let cache_key = glyph_mask_cache_key(
3706 font_hash,
3707 &glyph,
3708 raster_style,
3709 weight_synthesis,
3710 style_synthesis,
3711 );
3712 let Some((key, x, y, width, height)) =
3713 glyph_cache.get_atlas_placement(&cache_key, &glyph)
3714 else {
3715 if font.outline(glyph.id).is_none() {
3716 continue;
3717 }
3718 return f32::NAN;
3719 };
3720 visit(SoftwareGlyphAtlasPlacement {
3721 key,
3722 x,
3723 y,
3724 width,
3725 height,
3726 color: Color::WHITE,
3727 });
3728 }
3729 max_advance = max_advance.max((caret_x - origin_x).max(0.0));
3730 }
3731 max_advance
3732}
3733
3734#[allow(clippy::too_many_arguments)]
3735fn visit_text_glyph_atlas_run(
3736 text: &str,
3737 font: &impl Font,
3738 font_hash: u64,
3739 font_px_size: f32,
3740 line_height: f32,
3741 first_baseline_y: f32,
3742 origin_x: f32,
3743 origin_y: f32,
3744 letter_spacing: f32,
3745 raster_style: GlyphRasterStyle,
3746 weight_synthesis: TextWeightSynthesis,
3747 style_synthesis: TextStyleSynthesis,
3748 glyph_cache: &mut SoftwareGlyphRasterCache,
3749 mut visit: impl FnMut(SoftwareGlyphAtlasRunGlyph),
3750) -> f32 {
3751 let scale = PxScale::from(font_px_size);
3752 let scaled_font = font.as_scaled(scale);
3753 let mut max_advance = 0.0f32;
3754 let mut run_metrics_cache: Vec<(GlyphMaskCacheKey, CachedAtlasGlyphMetrics)> = Vec::new();
3755 for (line_idx, line) in text.split('\n').enumerate() {
3756 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
3757 let mut caret_x = origin_x;
3758 let mut previous = None;
3759 for ch in line.chars() {
3760 let glyph_id = scaled_font.glyph_id(ch);
3761 if let Some(previous_id) = previous {
3762 caret_x += scaled_font.kern(previous_id, glyph_id) + letter_spacing;
3763 }
3764 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
3765 caret_x += scaled_font.h_advance(glyph_id);
3766 previous = Some(glyph_id);
3767 let glyph = align_glyph_for_text_motion(glyph, true);
3768 let cache_key = glyph_mask_cache_key(
3769 font_hash,
3770 &glyph,
3771 raster_style,
3772 weight_synthesis,
3773 style_synthesis,
3774 );
3775 if let Some((_, metrics)) = run_metrics_cache
3776 .iter()
3777 .find(|(cached_key, _)| *cached_key == cache_key)
3778 {
3779 visit(SoftwareGlyphAtlasRunGlyph::Cached(
3780 metrics.placement(&glyph, Color::WHITE),
3781 ));
3782 continue;
3783 }
3784 if let Some(metrics) = glyph_cache.get_atlas_metrics(&cache_key) {
3785 if run_metrics_cache.len() < RUN_GLYPH_METRICS_CACHE_LIMIT {
3786 run_metrics_cache.push((cache_key, metrics));
3787 }
3788 visit(SoftwareGlyphAtlasRunGlyph::Cached(
3789 metrics.placement(&glyph, Color::WHITE),
3790 ));
3791 continue;
3792 }
3793
3794 if font.outline(glyph.id).is_none() {
3795 continue;
3796 }
3797 let Some(mask) = build_complete_glyph_mask(
3798 font,
3799 &glyph,
3800 raster_style,
3801 weight_synthesis,
3802 style_synthesis,
3803 ) else {
3804 continue;
3805 };
3806 let mask = glyph_cache.put(cache_key, &glyph, mask);
3807 let Some(key) = glyph_atlas_key_from_mask_key(cache_key) else {
3808 continue;
3809 };
3810 let (glyph_x, glyph_y) = static_glyph_pixel_origin(&glyph);
3811 if run_metrics_cache.len() < RUN_GLYPH_METRICS_CACHE_LIMIT {
3812 run_metrics_cache.push((
3813 cache_key,
3814 CachedAtlasGlyphMetrics {
3815 key,
3816 width: mask.width,
3817 height: mask.height,
3818 origin_offset_x: mask.origin_x - glyph_x,
3819 origin_offset_y: mask.origin_y - glyph_y,
3820 },
3821 ));
3822 }
3823 visit(SoftwareGlyphAtlasRunGlyph::New(SoftwareGlyphAtlasGlyph {
3824 key,
3825 mask: SoftwareGlyphAtlasMask {
3826 alpha: Arc::clone(&mask.alpha),
3827 width: mask.width,
3828 height: mask.height,
3829 },
3830 x: mask.origin_x,
3831 y: mask.origin_y,
3832 color: Color::WHITE,
3833 }));
3834 }
3835 max_advance = max_advance.max((caret_x - origin_x).max(0.0));
3836 }
3837 max_advance
3838}
3839
3840fn blend_src_over(dst: &mut [f32; 4], src: [f32; 4]) {
3841 let src_alpha = src[3].clamp(0.0, 1.0);
3842 if src_alpha <= 0.0 {
3843 return;
3844 }
3845
3846 let dst_alpha = dst[3].clamp(0.0, 1.0);
3847 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
3848
3849 if out_alpha <= f32::EPSILON {
3850 *dst = [0.0, 0.0, 0.0, 0.0];
3851 return;
3852 }
3853
3854 for channel in 0..3 {
3855 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
3856 let dst_premult = dst[channel].clamp(0.0, 1.0) * dst_alpha;
3857 dst[channel] =
3858 ((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0);
3859 }
3860 dst[3] = out_alpha;
3861}
3862
3863fn draw_mask_glyph(
3864 canvas: &mut [[f32; 4]],
3865 width: u32,
3866 height: u32,
3867 mask: &GlyphMask,
3868 brush: &Brush,
3869 brush_alpha_multiplier: f32,
3870 brush_rect: Rect,
3871) {
3872 for y in 0..mask.height {
3873 let py = mask.origin_y + y as i32;
3874 if py < 0 || py >= height as i32 {
3875 continue;
3876 }
3877
3878 for x in 0..mask.width {
3879 let px = mask.origin_x + x as i32;
3880 if px < 0 || px >= width as i32 {
3881 continue;
3882 }
3883
3884 let coverage = mask.alpha[y * mask.width + x];
3885 if coverage <= 0.0 {
3886 continue;
3887 }
3888
3889 let sample = sample_brush_rgba(
3890 brush,
3891 brush_rect,
3892 brush_rect.x + px as f32 + 0.5,
3893 brush_rect.y + py as f32 + 0.5,
3894 );
3895 let alpha = coverage * sample[3] * brush_alpha_multiplier;
3896 if alpha <= 0.0 {
3897 continue;
3898 }
3899 let idx = (py as u32 * width + px as u32) as usize;
3900 blend_src_over(
3901 &mut canvas[idx],
3902 [sample[0], sample[1], sample[2], alpha.clamp(0.0, 1.0)],
3903 );
3904 }
3905 }
3906}
3907
3908fn blend_src_over_u8(dst: &mut [u8], src: [f32; 4]) {
3909 let src_alpha = src[3].clamp(0.0, 1.0);
3910 if src_alpha <= 0.0 {
3911 return;
3912 }
3913
3914 let dst_alpha = dst[3] as f32 / 255.0;
3915 if dst_alpha <= 0.0 {
3916 dst[0] = (src[0].clamp(0.0, 1.0) * 255.0).round() as u8;
3917 dst[1] = (src[1].clamp(0.0, 1.0) * 255.0).round() as u8;
3918 dst[2] = (src[2].clamp(0.0, 1.0) * 255.0).round() as u8;
3919 dst[3] = (src_alpha * 255.0).round() as u8;
3920 return;
3921 }
3922
3923 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
3924 if out_alpha <= f32::EPSILON {
3925 dst.fill(0);
3926 return;
3927 }
3928
3929 for channel in 0..3 {
3930 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
3931 let dst_premult = (dst[channel] as f32 / 255.0) * dst_alpha;
3932 dst[channel] =
3933 ((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha * 255.0).round() as u8;
3934 }
3935 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
3936}
3937
3938fn draw_mask_glyph_solid_u8(
3939 canvas: &mut [u8],
3940 width: u32,
3941 height: u32,
3942 mask: &GlyphMask,
3943 color: [f32; 4],
3944 alpha_multiplier: f32,
3945) {
3946 let red = (color[0].clamp(0.0, 1.0) * 255.0).round() as u8;
3947 let green = (color[1].clamp(0.0, 1.0) * 255.0).round() as u8;
3948 let blue = (color[2].clamp(0.0, 1.0) * 255.0).round() as u8;
3949 let alpha_scale = color[3].clamp(0.0, 1.0) * alpha_multiplier.clamp(0.0, 1.0);
3950 if alpha_scale <= 0.0 {
3951 return;
3952 }
3953
3954 for y in 0..mask.height {
3955 let py = mask.origin_y + y as i32;
3956 if py < 0 || py >= height as i32 {
3957 continue;
3958 }
3959
3960 for x in 0..mask.width {
3961 let px = mask.origin_x + x as i32;
3962 if px < 0 || px >= width as i32 {
3963 continue;
3964 }
3965
3966 let coverage = mask.alpha[y * mask.width + x];
3967 if coverage <= 0.0 {
3968 continue;
3969 }
3970
3971 let alpha = (coverage * alpha_scale).clamp(0.0, 1.0);
3972 let alpha_u8 = (alpha * 255.0).round() as u8;
3973 if alpha_u8 == 0 {
3974 continue;
3975 }
3976 let idx = ((py as u32 * width + px as u32) * 4) as usize;
3977 let dst = &mut canvas[idx..idx + 4];
3978 if dst[3] == 0 {
3979 dst[0] = red;
3980 dst[1] = green;
3981 dst[2] = blue;
3982 dst[3] = alpha_u8;
3983 } else {
3984 blend_src_over_u8(dst, [color[0], color[1], color[2], alpha]);
3985 }
3986 }
3987 }
3988}
3989
3990fn draw_shadow_mask(
3991 canvas: &mut [[f32; 4]],
3992 width: u32,
3993 height: u32,
3994 mask: &GlyphMask,
3995 shadow: Shadow,
3996 text_scale: f32,
3997 static_text_motion: bool,
3998) {
3999 if mask.width == 0 || mask.height == 0 {
4000 return;
4001 }
4002
4003 let shadow_dx = shadow.offset.x * text_scale;
4004 let shadow_dy = shadow.offset.y * text_scale;
4005 let blur_radius = (shadow.blur_radius * text_scale).max(0.0);
4006 let sigma = shadow_blur_sigma(blur_radius);
4007 let blur_margin = if sigma > 0.0 {
4008 (sigma * 3.0).ceil() as i32
4009 } else {
4010 0
4011 };
4012
4013 let padded_width = mask.width + (blur_margin as usize) * 2;
4014 let padded_height = mask.height + (blur_margin as usize) * 2;
4015 let mut padded_mask = vec![0.0f32; padded_width * padded_height];
4016
4017 for y in 0..mask.height {
4018 let src_offset = y * mask.width;
4019 let dst_offset = (y + blur_margin as usize) * padded_width + blur_margin as usize;
4020 padded_mask[dst_offset..dst_offset + mask.width]
4021 .copy_from_slice(&mask.alpha[src_offset..src_offset + mask.width]);
4022 }
4023
4024 let blurred = if sigma > 0.0 {
4025 gaussian_blur_alpha(&padded_mask, padded_width, padded_height, sigma)
4026 } else {
4027 padded_mask
4028 };
4029
4030 let shadow_rgba = color_to_rgba(shadow.color);
4031 let shadow_origin_x = mask.origin_x - blur_margin;
4032 let shadow_origin_y = mask.origin_y - blur_margin;
4033
4034 for y in 0..padded_height {
4035 for x in 0..padded_width {
4036 let alpha = blurred[y * padded_width + x] * shadow_rgba[3];
4037 if alpha <= 0.0 {
4038 continue;
4039 }
4040
4041 let target_x = shadow_origin_x as f32 + x as f32 + shadow_dx;
4042 let target_y = shadow_origin_y as f32 + y as f32 + shadow_dy;
4043 if static_text_motion {
4044 blend_shadow_pixel(
4045 canvas,
4046 width,
4047 height,
4048 target_x.round() as i32,
4049 target_y.round() as i32,
4050 shadow_rgba,
4051 alpha.clamp(0.0, 1.0),
4052 );
4053 } else {
4054 blend_shadow_pixel_subpixel(
4055 canvas,
4056 width,
4057 height,
4058 target_x,
4059 target_y,
4060 shadow_rgba,
4061 alpha.clamp(0.0, 1.0),
4062 );
4063 }
4064 }
4065 }
4066}
4067
4068fn blend_shadow_pixel(
4069 canvas: &mut [[f32; 4]],
4070 width: u32,
4071 height: u32,
4072 px: i32,
4073 py: i32,
4074 color: [f32; 4],
4075 alpha: f32,
4076) {
4077 if px < 0 || py < 0 || px >= width as i32 || py >= height as i32 || alpha <= 0.0 {
4078 return;
4079 }
4080 let idx = (py as u32 * width + px as u32) as usize;
4081 blend_src_over(
4082 &mut canvas[idx],
4083 [color[0], color[1], color[2], alpha.clamp(0.0, 1.0)],
4084 );
4085}
4086
4087fn blend_shadow_pixel_subpixel(
4088 canvas: &mut [[f32; 4]],
4089 width: u32,
4090 height: u32,
4091 x: f32,
4092 y: f32,
4093 color: [f32; 4],
4094 alpha: f32,
4095) {
4096 if alpha <= 0.0 {
4097 return;
4098 }
4099
4100 let base_x = x.floor();
4101 let base_y = y.floor();
4102 let frac_x = x - base_x;
4103 let frac_y = y - base_y;
4104 let base_x_i32 = base_x as i32;
4105 let base_y_i32 = base_y as i32;
4106 let weights = [
4107 ((1.0 - frac_x) * (1.0 - frac_y), 0i32, 0i32),
4108 (frac_x * (1.0 - frac_y), 1, 0),
4109 ((1.0 - frac_x) * frac_y, 0, 1),
4110 (frac_x * frac_y, 1, 1),
4111 ];
4112
4113 for (weight, dx, dy) in weights {
4114 if weight <= 0.0 {
4115 continue;
4116 }
4117 blend_shadow_pixel(
4118 canvas,
4119 width,
4120 height,
4121 base_x_i32 + dx,
4122 base_y_i32 + dy,
4123 color,
4124 alpha * weight,
4125 );
4126 }
4127}
4128
4129fn shadow_blur_sigma(blur_radius: f32) -> f32 {
4130 if blur_radius <= 0.0 {
4131 0.0
4132 } else {
4133 (blur_radius * SHADOW_SIGMA_SCALE + SHADOW_SIGMA_BIAS).max(0.5)
4134 }
4135}
4136
4137fn gaussian_blur_alpha(src: &[f32], width: usize, height: usize, sigma: f32) -> Vec<f32> {
4138 let kernel = gaussian_kernel_1d(sigma);
4139 if kernel.len() == 1 {
4140 return src.to_vec();
4141 }
4142 let half = (kernel.len() / 2) as i32;
4143
4144 let mut horizontal = vec![0.0f32; src.len()];
4145 for y in 0..height {
4146 for x in 0..width {
4147 let mut sum = 0.0f32;
4148 for (index, weight) in kernel.iter().enumerate() {
4149 let offset = index as i32 - half;
4150 let sample_x = (x as i32 + offset).clamp(0, width as i32 - 1) as usize;
4151 sum += src[y * width + sample_x] * *weight;
4152 }
4153 horizontal[y * width + x] = sum;
4154 }
4155 }
4156
4157 let mut output = vec![0.0f32; src.len()];
4158 for y in 0..height {
4159 for x in 0..width {
4160 let mut sum = 0.0f32;
4161 for (index, weight) in kernel.iter().enumerate() {
4162 let offset = index as i32 - half;
4163 let sample_y = (y as i32 + offset).clamp(0, height as i32 - 1) as usize;
4164 sum += horizontal[sample_y * width + x] * *weight;
4165 }
4166 output[y * width + x] = sum;
4167 }
4168 }
4169
4170 output
4171}
4172
4173fn gaussian_kernel_1d(sigma: f32) -> Vec<f32> {
4174 let half = ((sigma * 3.0).ceil() as i32).clamp(1, MAX_GAUSSIAN_KERNEL_HALF);
4175 if half <= 0 {
4176 return vec![1.0];
4177 }
4178
4179 let mut kernel = Vec::with_capacity((half * 2 + 1) as usize);
4180 let mut sum = 0.0f32;
4181 for offset in -half..=half {
4182 let distance = offset as f32;
4183 let weight = (-0.5 * (distance / sigma).powi(2)).exp();
4184 kernel.push(weight);
4185 sum += weight;
4186 }
4187
4188 if sum > f32::EPSILON {
4189 for weight in &mut kernel {
4190 *weight /= sum;
4191 }
4192 }
4193
4194 kernel
4195}
4196
4197fn outline_glyph_with_bounds(
4198 font: &impl Font,
4199 glyph: &Glyph,
4200) -> Option<(OutlinedGlyph, GlyphPixelBounds)> {
4201 let outlined = font.outline_glyph(glyph.clone())?;
4202 let bounds = pixel_bounds_from_outlined(&outlined);
4203 Some((outlined, bounds))
4204}
4205
4206fn build_glyph_mask(
4207 font: &impl Font,
4208 glyph: &Glyph,
4209 outlined: &OutlinedGlyph,
4210 bounds: GlyphPixelBounds,
4211 style: GlyphRasterStyle,
4212) -> Option<GlyphMask> {
4213 match style {
4214 GlyphRasterStyle::Fill => build_fill_mask(outlined, bounds),
4215 GlyphRasterStyle::Stroke { width_px } => {
4216 build_stroke_mask(font, glyph, outlined, bounds, width_px)
4217 }
4218 }
4219}
4220
4221fn build_fill_mask(outlined: &OutlinedGlyph, bounds: GlyphPixelBounds) -> Option<GlyphMask> {
4222 let mask_width = bounds.width();
4223 let mask_height = bounds.height();
4224 if mask_width == 0 || mask_height == 0 {
4225 return None;
4226 }
4227
4228 let mut alpha = vec![0.0f32; mask_width * mask_height];
4229 outlined.draw(|gx, gy, value| {
4230 let idx = gy as usize * mask_width + gx as usize;
4231 alpha[idx] = value;
4232 });
4233
4234 Some(GlyphMask {
4235 alpha: Arc::from(alpha),
4236 width: mask_width,
4237 height: mask_height,
4238 origin_x: bounds.min_x,
4239 origin_y: bounds.min_y,
4240 })
4241}
4242
4243fn build_stroke_mask(
4244 font: &impl Font,
4245 glyph: &Glyph,
4246 outlined: &OutlinedGlyph,
4247 bounds: GlyphPixelBounds,
4248 stroke_width_px: f32,
4249) -> Option<GlyphMask> {
4250 if !stroke_width_px.is_finite() || stroke_width_px <= 0.0 {
4251 return build_fill_mask(outlined, bounds);
4252 }
4253
4254 let mask_width = bounds.max_x - bounds.min_x;
4255 let mask_height = bounds.max_y - bounds.min_y;
4256 if mask_width <= 0 || mask_height <= 0 {
4257 return None;
4258 }
4259
4260 let half_width = stroke_width_px * 0.5;
4261 let miter_pad = (half_width * COMPOSE_STROKE_MITER_LIMIT).ceil();
4262 let pad = miter_pad.max(1.0) as i32 + 1;
4263 let path = build_outline_path(font, glyph, bounds, pad)?;
4264 let raster_width = mask_width + pad * 2;
4265 let raster_height = mask_height + pad * 2;
4266 if raster_width <= 0 || raster_height <= 0 {
4267 return None;
4268 }
4269
4270 let mut pixmap = Pixmap::new(raster_width as u32, raster_height as u32)?;
4271 let mut paint = Paint::default();
4272 paint.set_color_rgba8(255, 255, 255, 255);
4273 paint.anti_alias = true;
4274
4275 let stroke = Stroke {
4276 width: stroke_width_px,
4277 line_cap: LineCap::Butt,
4278 line_join: LineJoin::Miter,
4279 miter_limit: COMPOSE_STROKE_MITER_LIMIT,
4280 ..Stroke::default()
4281 };
4282
4283 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
4284
4285 let alpha: Vec<f32> = pixmap
4286 .data()
4287 .chunks_exact(4)
4288 .map(|pixel| pixel[3] as f32 / 255.0)
4289 .collect();
4290
4291 Some(GlyphMask {
4292 alpha: Arc::from(alpha),
4293 width: raster_width as usize,
4294 height: raster_height as usize,
4295 origin_x: bounds.min_x - pad,
4296 origin_y: bounds.min_y - pad,
4297 })
4298}
4299
4300fn synthesize_glyph_weight(mask: GlyphMask, synthesis: TextWeightSynthesis) -> GlyphMask {
4301 let horizontal_shift = synthetic_weight_shift_px(synthesis.embolden_px);
4302 if horizontal_shift == 0 || mask.width == 0 || mask.height == 0 {
4303 return mask;
4304 }
4305
4306 let vertical_shift = (horizontal_shift / 2).min(1);
4307 let output_width = mask.width + horizontal_shift;
4308 let output_height = mask.height + vertical_shift * 2;
4309 let mut alpha = vec![0.0f32; output_width * output_height];
4310 for y in 0..mask.height {
4311 for x in 0..mask.width {
4312 let coverage = mask.alpha[y * mask.width + x];
4313 if coverage <= 0.0 {
4314 continue;
4315 }
4316 for dy in 0..=(vertical_shift * 2) {
4317 let output_y = y + dy;
4318 for dx in 0..=horizontal_shift {
4319 let output_x = x + dx;
4320 let output_index = output_y * output_width + output_x;
4321 if coverage > alpha[output_index] {
4322 alpha[output_index] = coverage;
4323 }
4324 }
4325 }
4326 }
4327 }
4328
4329 GlyphMask {
4330 alpha: Arc::from(alpha),
4331 width: output_width,
4332 height: output_height,
4333 origin_x: mask.origin_x,
4334 origin_y: mask.origin_y - vertical_shift as i32,
4335 }
4336}
4337
4338fn synthesize_glyph_style(mask: GlyphMask, synthesis: TextStyleSynthesis) -> GlyphMask {
4339 if synthesis.slant <= 0.0 || mask.width == 0 || mask.height == 0 {
4340 return mask;
4341 }
4342
4343 let max_shift = ((mask.height.saturating_sub(1)) as f32 * synthesis.slant).ceil() as usize;
4344 if max_shift == 0 {
4345 return mask;
4346 }
4347
4348 let output_width = mask.width + max_shift + 1;
4349 let mut alpha = vec![0.0f32; output_width * mask.height];
4350 for y in 0..mask.height {
4351 let shift = (mask.height.saturating_sub(1) - y) as f32 * synthesis.slant;
4352 let shift_floor = shift.floor() as usize;
4353 let shift_fraction = shift - shift.floor();
4354 for x in 0..mask.width {
4355 let coverage = mask.alpha[y * mask.width + x];
4356 if coverage <= 0.0 {
4357 continue;
4358 }
4359
4360 let output_x = x + shift_floor;
4361 let left_index = y * output_width + output_x;
4362 let left_coverage = coverage * (1.0 - shift_fraction);
4363 if left_coverage > alpha[left_index] {
4364 alpha[left_index] = left_coverage;
4365 }
4366
4367 if shift_fraction > 0.0 {
4368 let right_index = left_index + 1;
4369 let right_coverage = coverage * shift_fraction;
4370 if right_coverage > alpha[right_index] {
4371 alpha[right_index] = right_coverage;
4372 }
4373 }
4374 }
4375 }
4376
4377 GlyphMask {
4378 alpha: Arc::from(alpha),
4379 width: output_width,
4380 height: mask.height,
4381 origin_x: mask.origin_x,
4382 origin_y: mask.origin_y,
4383 }
4384}
4385
4386fn synthetic_weight_shift_px(embolden_px: f32) -> usize {
4387 if !embolden_px.is_finite() || embolden_px < 0.35 {
4388 return 0;
4389 }
4390 embolden_px.ceil().max(1.0) as usize
4391}
4392
4393fn build_outline_path(
4394 font: &impl Font,
4395 glyph: &Glyph,
4396 bounds: GlyphPixelBounds,
4397 pad: i32,
4398) -> Option<Path> {
4399 let outline = font.outline(glyph.id)?;
4400 let scale_factor = font.as_scaled(glyph.scale).scale_factor();
4401 let mut builder = PathBuilder::new();
4402 let mut has_segments = false;
4403 let mut current_end = None;
4404 let mut subpath_start = None;
4405
4406 for curve in outline.curves {
4407 match curve {
4408 ab_glyph::OutlineCurve::Line(p0, p1) => {
4409 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
4410 let end = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
4411 if current_end != Some(start) {
4412 if current_end.is_some() {
4413 builder.close();
4414 }
4415 builder.move_to(start.0, start.1);
4416 subpath_start = Some(start);
4417 }
4418 builder.line_to(end.0, end.1);
4419 if subpath_start == Some(end) {
4420 builder.close();
4421 current_end = None;
4422 subpath_start = None;
4423 } else {
4424 current_end = Some(end);
4425 }
4426 }
4427 ab_glyph::OutlineCurve::Quad(p0, p1, p2) => {
4428 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
4429 let control = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
4430 let end = transform_outline_point(p2, scale_factor, glyph, bounds, pad);
4431 if current_end != Some(start) {
4432 if current_end.is_some() {
4433 builder.close();
4434 }
4435 builder.move_to(start.0, start.1);
4436 subpath_start = Some(start);
4437 }
4438 builder.quad_to(control.0, control.1, end.0, end.1);
4439 if subpath_start == Some(end) {
4440 builder.close();
4441 current_end = None;
4442 subpath_start = None;
4443 } else {
4444 current_end = Some(end);
4445 }
4446 }
4447 ab_glyph::OutlineCurve::Cubic(p0, p1, p2, p3) => {
4448 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
4449 let control1 = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
4450 let control2 = transform_outline_point(p2, scale_factor, glyph, bounds, pad);
4451 let end = transform_outline_point(p3, scale_factor, glyph, bounds, pad);
4452 if current_end != Some(start) {
4453 if current_end.is_some() {
4454 builder.close();
4455 }
4456 builder.move_to(start.0, start.1);
4457 subpath_start = Some(start);
4458 }
4459 builder.cubic_to(control1.0, control1.1, control2.0, control2.1, end.0, end.1);
4460 if subpath_start == Some(end) {
4461 builder.close();
4462 current_end = None;
4463 subpath_start = None;
4464 } else {
4465 current_end = Some(end);
4466 }
4467 }
4468 }
4469 has_segments = true;
4470 }
4471
4472 if !has_segments {
4473 return None;
4474 }
4475
4476 if current_end.is_some() {
4477 builder.close();
4478 }
4479
4480 builder.finish()
4481}
4482
4483fn transform_outline_point(
4484 point: ab_glyph::Point,
4485 scale_factor: ab_glyph::PxScaleFactor,
4486 glyph: &Glyph,
4487 bounds: GlyphPixelBounds,
4488 pad: i32,
4489) -> (f32, f32) {
4490 (
4491 point.x * scale_factor.horizontal + glyph.position.x - bounds.min_x as f32 + pad as f32,
4492 point.y * -scale_factor.vertical + glyph.position.y - bounds.min_y as f32 + pad as f32,
4493 )
4494}
4495
4496#[cfg(test)]
4497mod tests {
4498 use super::*;
4499 use cranpose_ui::text::{RangeStyle, SpanStyle};
4500 use cranpose_ui_graphics::Point;
4501
4502 fn count_ink_pixels(image: &ImageBitmap) -> usize {
4503 image
4504 .pixels()
4505 .chunks_exact(4)
4506 .filter(|px| px[3] > 0)
4507 .count()
4508 }
4509
4510 #[test]
4511 fn software_glyph_raster_cache_reuses_static_masks_across_positions() {
4512 let font = default_software_text_font().expect("bundled default font");
4513 let style = TextStyle::default();
4514 let rect = Rect {
4515 x: 0.0,
4516 y: 0.0,
4517 width: 160.0,
4518 height: 32.0,
4519 };
4520 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4521
4522 let uncached = rasterize_text_to_image(
4523 "aaaa",
4524 rect,
4525 &style,
4526 Color(1.0, 1.0, 1.0, 1.0),
4527 18.0,
4528 1.0,
4529 &font,
4530 )
4531 .expect("uncached image");
4532 let cached = rasterize_text_to_image_with_glyph_cache(
4533 "aaaa",
4534 rect,
4535 &style,
4536 Color(1.0, 1.0, 1.0, 1.0),
4537 18.0,
4538 1.0,
4539 &font,
4540 &mut cache,
4541 )
4542 .expect("cached image");
4543
4544 assert_eq!(cached.pixels(), uncached.pixels());
4545 let stats = cache.stats();
4546 assert_eq!(stats.entries, 1);
4547 assert_eq!(stats.misses, 1);
4548 assert_eq!(stats.hits, 3);
4549
4550 let shifted_rect = Rect {
4551 x: 24.0,
4552 y: 17.0,
4553 ..rect
4554 };
4555 let _ = rasterize_text_to_image_with_glyph_cache(
4556 "aaaa",
4557 shifted_rect,
4558 &style,
4559 Color(1.0, 1.0, 1.0, 1.0),
4560 18.0,
4561 1.0,
4562 &font,
4563 &mut cache,
4564 )
4565 .expect("cached shifted image");
4566
4567 let shifted_stats = cache.stats();
4568 assert_eq!(shifted_stats.entries, 1);
4569 assert_eq!(shifted_stats.misses, 1);
4570 assert_eq!(shifted_stats.hits, 7);
4571 }
4572
4573 #[test]
4574 fn annotated_solid_text_direct_raster_matches_plain_text_pixels() {
4575 let font = default_software_text_font().expect("bundled default font");
4576 let font_set = SoftwareTextFontSet::from_font(font.clone());
4577 let style = TextStyle::default();
4578 let rect = Rect {
4579 x: 0.0,
4580 y: 0.0,
4581 width: 240.0,
4582 height: 40.0,
4583 };
4584 let color = Color(1.0, 1.0, 1.0, 1.0);
4585 let annotated = AnnotatedString {
4586 text: "plain link".to_string(),
4587 span_styles: vec![RangeStyle {
4588 item: SpanStyle {
4589 color: Some(color),
4590 ..Default::default()
4591 },
4592 range: 0..10,
4593 }],
4594 ..Default::default()
4595 };
4596 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4597
4598 let plain = rasterize_text_to_image(
4599 annotated.text.as_str(),
4600 rect,
4601 &style,
4602 color,
4603 18.0,
4604 1.0,
4605 &font,
4606 )
4607 .expect("plain text image");
4608 let direct = rasterize_annotated_text_to_image_with_glyph_cache(
4609 &annotated, rect, &style, color, 18.0, 1.0, &font_set, &mut cache,
4610 )
4611 .expect("annotated text image");
4612
4613 assert_eq!(direct.pixels(), plain.pixels());
4614 }
4615
4616 #[test]
4617 fn solid_annotated_text_collects_atlas_glyphs_with_stable_keys() {
4618 let font = default_software_text_font().expect("bundled default font");
4619 let font_set = SoftwareTextFontSet::from_font(font);
4620 let style = TextStyle::default();
4621 let rect = Rect {
4622 x: 12.0,
4623 y: 4.0,
4624 width: 260.0,
4625 height: 48.0,
4626 };
4627 let annotated = AnnotatedString {
4628 text: "markdown link".to_string(),
4629 span_styles: vec![RangeStyle {
4630 item: SpanStyle {
4631 color: Some(Color(0.4, 0.7, 1.0, 1.0)),
4632 ..Default::default()
4633 },
4634 range: 9..13,
4635 }],
4636 ..Default::default()
4637 };
4638 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4639 let mut glyphs = Vec::new();
4640
4641 collect_solid_text_atlas_glyphs(
4642 &annotated,
4643 rect,
4644 &style,
4645 Color::WHITE,
4646 18.0,
4647 1.0,
4648 &font_set,
4649 &mut cache,
4650 &mut glyphs,
4651 )
4652 .expect("solid styled text is atlas-eligible");
4653
4654 assert!(!glyphs.is_empty());
4655 assert!(glyphs.iter().all(|glyph| glyph.mask.width > 0));
4656 assert!(glyphs.iter().all(|glyph| glyph.mask.height > 0));
4657 assert!(glyphs
4658 .iter()
4659 .any(|glyph| glyph.color == Color(0.4, 0.7, 1.0, 1.0)));
4660 assert!(cache.stats().entries > 0);
4661 }
4662
4663 #[test]
4664 fn cached_atlas_placements_reuse_existing_glyph_masks_without_payloads() {
4665 let font = default_software_text_font().expect("bundled default font");
4666 let font_set = SoftwareTextFontSet::from_font(font);
4667 let style = TextStyle::default();
4668 let rect = Rect {
4669 x: 12.0,
4670 y: 4.0,
4671 width: 260.0,
4672 height: 48.0,
4673 };
4674 let annotated = AnnotatedString {
4675 text: "markdown link".to_string(),
4676 span_styles: vec![RangeStyle {
4677 item: SpanStyle {
4678 color: Some(Color(0.4, 0.7, 1.0, 1.0)),
4679 ..Default::default()
4680 },
4681 range: 9..13,
4682 }],
4683 ..Default::default()
4684 };
4685 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4686 let mut placements = Vec::new();
4687
4688 assert!(
4689 collect_cached_solid_text_atlas_placements(
4690 &annotated,
4691 rect,
4692 &style,
4693 Color::WHITE,
4694 18.0,
4695 1.0,
4696 &font_set,
4697 &mut cache,
4698 &mut placements,
4699 )
4700 .is_none(),
4701 "placement-only collection requires retained glyph masks"
4702 );
4703 assert!(placements.is_empty());
4704
4705 let mut glyphs = Vec::new();
4706 collect_solid_text_atlas_glyphs(
4707 &annotated,
4708 rect,
4709 &style,
4710 Color::WHITE,
4711 18.0,
4712 1.0,
4713 &font_set,
4714 &mut cache,
4715 &mut glyphs,
4716 )
4717 .expect("solid styled text is atlas-eligible");
4718
4719 collect_cached_solid_text_atlas_placements(
4720 &annotated,
4721 rect,
4722 &style,
4723 Color::WHITE,
4724 18.0,
4725 1.0,
4726 &font_set,
4727 &mut cache,
4728 &mut placements,
4729 )
4730 .expect("cached masks provide placement-only atlas glyphs");
4731
4732 assert_eq!(placements.len(), glyphs.len());
4733 assert!(placements
4734 .iter()
4735 .zip(glyphs.iter())
4736 .all(|(placement, glyph)| {
4737 placement.key == glyph.key
4738 && placement.x == glyph.x
4739 && placement.y == glyph.y
4740 && placement.width == glyph.mask.width
4741 && placement.height == glyph.mask.height
4742 && placement.color == glyph.color
4743 }));
4744 let recovered = cache
4745 .atlas_glyph_for_placement(&placements[0])
4746 .expect("placement should recover retained mask payload");
4747 assert_eq!(recovered.key, glyphs[0].key);
4748 assert_eq!(recovered.x, glyphs[0].x);
4749 assert_eq!(recovered.y, glyphs[0].y);
4750 assert_eq!(recovered.mask.width, glyphs[0].mask.width);
4751 assert_eq!(recovered.mask.height, glyphs[0].mask.height);
4752 assert_eq!(recovered.mask.alpha, glyphs[0].mask.alpha);
4753 assert_eq!(recovered.color, glyphs[0].color);
4754 }
4755
4756 #[test]
4757 fn atlas_glyph_collection_rejects_shadow_and_gradient_without_partial_output() {
4758 let font = default_software_text_font().expect("bundled default font");
4759 let font_set = SoftwareTextFontSet::from_font(font);
4760 let rect = Rect {
4761 x: 0.0,
4762 y: 0.0,
4763 width: 240.0,
4764 height: 40.0,
4765 };
4766 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4767 let mut glyphs = Vec::new();
4768 glyphs.push(SoftwareGlyphAtlasGlyph {
4769 key: SoftwareGlyphAtlasKey {
4770 font_hash: 1,
4771 glyph_id: 1,
4772 scale_x_bits: 1,
4773 scale_y_bits: 1,
4774 embolden_px_bits: 0,
4775 slant_bits: 0,
4776 },
4777 mask: SoftwareGlyphAtlasMask {
4778 alpha: Arc::from([1.0f32]),
4779 width: 1,
4780 height: 1,
4781 },
4782 x: 0,
4783 y: 0,
4784 color: Color::WHITE,
4785 });
4786 let initial_len = glyphs.len();
4787
4788 let shadow_style = TextStyle::from_span_style(SpanStyle {
4789 shadow: Some(Shadow {
4790 color: Color(0.0, 0.0, 0.0, 0.5),
4791 offset: Point::new(1.0, 1.0),
4792 blur_radius: 0.0,
4793 }),
4794 ..Default::default()
4795 });
4796 assert!(collect_solid_text_atlas_glyphs(
4797 &AnnotatedString::new("shadow".to_string()),
4798 rect,
4799 &shadow_style,
4800 Color::WHITE,
4801 18.0,
4802 1.0,
4803 &font_set,
4804 &mut cache,
4805 &mut glyphs,
4806 )
4807 .is_none());
4808 assert_eq!(glyphs.len(), initial_len);
4809
4810 let gradient_style = TextStyle::from_span_style(SpanStyle {
4811 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
4812 ..Default::default()
4813 });
4814 assert!(collect_solid_text_atlas_glyphs(
4815 &AnnotatedString::new("gradient".to_string()),
4816 rect,
4817 &gradient_style,
4818 Color::WHITE,
4819 18.0,
4820 1.0,
4821 &font_set,
4822 &mut cache,
4823 &mut glyphs,
4824 )
4825 .is_none());
4826 assert_eq!(glyphs.len(), initial_len);
4827 }
4828
4829 fn average_ink_rgb(
4830 image: &ImageBitmap,
4831 x_start: u32,
4832 x_end: u32,
4833 y_start: u32,
4834 y_end: u32,
4835 ) -> Option<[f32; 3]> {
4836 let width = image.width();
4837 let height = image.height();
4838 let mut sums = [0.0f32; 3];
4839 let mut count = 0usize;
4840 let pixels = image.pixels();
4841
4842 let x_end = x_end.min(width);
4843 let y_end = y_end.min(height);
4844 for y in y_start.min(height)..y_end {
4845 for x in x_start.min(width)..x_end {
4846 let idx = ((y * width + x) * 4) as usize;
4847 let alpha = pixels[idx + 3];
4848 if alpha == 0 {
4849 continue;
4850 }
4851 sums[0] += pixels[idx] as f32 / 255.0;
4852 sums[1] += pixels[idx + 1] as f32 / 255.0;
4853 sums[2] += pixels[idx + 2] as f32 / 255.0;
4854 count += 1;
4855 }
4856 }
4857
4858 if count == 0 {
4859 return None;
4860 }
4861 Some([
4862 sums[0] / count as f32,
4863 sums[1] / count as f32,
4864 sums[2] / count as f32,
4865 ])
4866 }
4867
4868 fn ink_x_range(image: &ImageBitmap) -> Option<(u32, u32)> {
4869 let width = image.width();
4870 let height = image.height();
4871 let pixels = image.pixels();
4872 let mut min_x = u32::MAX;
4873 let mut max_x = 0u32;
4874 let mut found = false;
4875 for y in 0..height {
4876 for x in 0..width {
4877 let idx = ((y * width + x) * 4) as usize;
4878 if pixels[idx + 3] > 0 {
4879 min_x = min_x.min(x);
4880 max_x = max_x.max(x + 1);
4881 found = true;
4882 }
4883 }
4884 }
4885 found.then_some((min_x, max_x))
4886 }
4887
4888 fn ink_y_range(image: &ImageBitmap) -> Option<(u32, u32)> {
4889 let width = image.width();
4890 let height = image.height();
4891 let pixels = image.pixels();
4892 let mut min_y = u32::MAX;
4893 let mut max_y = 0u32;
4894 let mut found = false;
4895 for y in 0..height {
4896 for x in 0..width {
4897 let idx = ((y * width + x) * 4) as usize;
4898 if pixels[idx + 3] > 0 {
4899 min_y = min_y.min(y);
4900 max_y = max_y.max(y + 1);
4901 found = true;
4902 }
4903 }
4904 }
4905 found.then_some((min_y, max_y))
4906 }
4907
4908 fn ink_centroid_x(image: &ImageBitmap, y_start: u32, y_end: u32) -> Option<f32> {
4909 let width = image.width();
4910 let height = image.height();
4911 let pixels = image.pixels();
4912 let mut weighted_x = 0.0f32;
4913 let mut total_alpha = 0.0f32;
4914
4915 for y in y_start.min(height)..y_end.min(height) {
4916 for x in 0..width {
4917 let idx = ((y * width + x) * 4) as usize;
4918 let alpha = pixels[idx + 3] as f32 / 255.0;
4919 if alpha <= 0.0 {
4920 continue;
4921 }
4922 weighted_x += x as f32 * alpha;
4923 total_alpha += alpha;
4924 }
4925 }
4926
4927 (total_alpha > 0.0).then_some(weighted_x / total_alpha)
4928 }
4929
4930 fn vertical_slant_delta(image: &ImageBitmap) -> f32 {
4931 let (top, bottom) = ink_y_range(image).expect("image should contain ink");
4932 let mid = top + (bottom - top).max(1) / 2;
4933 let top_x = ink_centroid_x(image, top, mid).expect("top ink centroid");
4934 let bottom_x = ink_centroid_x(image, mid, bottom).expect("bottom ink centroid");
4935 top_x - bottom_x
4936 }
4937
4938 fn top_ink_row(image: &ImageBitmap) -> Option<u32> {
4939 let width = image.width();
4940 let height = image.height();
4941 let pixels = image.pixels();
4942 for y in 0..height {
4943 for x in 0..width {
4944 let idx = ((y * width + x) * 4) as usize;
4945 if pixels[idx + 3] > 0 {
4946 return Some(y);
4947 }
4948 }
4949 }
4950 None
4951 }
4952
4953 fn reference_dilation_offsets(radius: i32) -> Vec<(i32, i32)> {
4954 let mut offsets = Vec::new();
4955 let squared_radius = radius * radius;
4956 for dy in -radius..=radius {
4957 for dx in -radius..=radius {
4958 if dx * dx + dy * dy <= squared_radius {
4959 offsets.push((dx, dy));
4960 }
4961 }
4962 }
4963 if offsets.is_empty() {
4964 offsets.push((0, 0));
4965 }
4966 offsets
4967 }
4968
4969 fn reference_dilation_stroke_mask(fill: &GlyphMask, stroke_width: f32) -> GlyphMask {
4970 let radius = (stroke_width * 0.5).ceil() as i32;
4971 let offsets = reference_dilation_offsets(radius);
4972 let out_width = fill.width as i32 + radius * 2;
4973 let out_height = fill.height as i32 + radius * 2;
4974 let fill_width_i32 = fill.width as i32;
4975 let fill_height_i32 = fill.height as i32;
4976 let mut alpha = vec![0.0f32; (out_width * out_height) as usize];
4977
4978 for out_y in 0..out_height {
4979 let oy = out_y - radius;
4980 for out_x in 0..out_width {
4981 let ox = out_x - radius;
4982 let base_alpha =
4983 if ox >= 0 && oy >= 0 && ox < fill_width_i32 && oy < fill_height_i32 {
4984 fill.alpha[oy as usize * fill.width + ox as usize]
4985 } else {
4986 0.0
4987 };
4988
4989 let mut dilated_alpha = 0.0f32;
4990 for (dx, dy) in &offsets {
4991 let sx = ox + dx;
4992 let sy = oy + dy;
4993 if sx < 0 || sy < 0 || sx >= fill_width_i32 || sy >= fill_height_i32 {
4994 continue;
4995 }
4996 let sample = fill.alpha[sy as usize * fill.width + sx as usize];
4997 if sample > dilated_alpha {
4998 dilated_alpha = sample;
4999 if dilated_alpha >= 0.999 {
5000 break;
5001 }
5002 }
5003 }
5004 alpha[out_y as usize * out_width as usize + out_x as usize] =
5005 (dilated_alpha - base_alpha).max(0.0);
5006 }
5007 }
5008
5009 GlyphMask {
5010 alpha: Arc::from(alpha),
5011 width: out_width as usize,
5012 height: out_height as usize,
5013 origin_x: fill.origin_x - radius,
5014 origin_y: fill.origin_y - radius,
5015 }
5016 }
5017
5018 fn rasterize_reference_dilation_stroke(
5019 text: &str,
5020 rect: Rect,
5021 font_size: f32,
5022 stroke_width: f32,
5023 font: &impl Font,
5024 ) -> ImageBitmap {
5025 let width = rect.width.ceil().max(1.0) as u32;
5026 let height = rect.height.ceil().max(1.0) as u32;
5027 let mut canvas = vec![[0.0f32; 4]; (width * height) as usize];
5028
5029 let metrics = vertical_metrics(font, font_size);
5030 let baseline = baseline_y_for_line_box(metrics, font_size * 1.4);
5031 for glyph in layout_line_glyphs(font, text, font_size, point(0.0, baseline)) {
5032 let Some((outlined, bounds)) = outline_glyph_with_bounds(font, &glyph) else {
5033 continue;
5034 };
5035 let Some(fill) = build_fill_mask(&outlined, bounds) else {
5036 continue;
5037 };
5038 let reference = reference_dilation_stroke_mask(&fill, stroke_width);
5039 draw_mask_glyph(
5040 &mut canvas,
5041 width,
5042 height,
5043 &reference,
5044 &Brush::solid(Color::WHITE),
5045 1.0,
5046 rect,
5047 );
5048 }
5049
5050 let mut rgba = vec![0u8; canvas.len() * 4];
5051 for (index, pixel) in canvas.iter().enumerate() {
5052 let base = index * 4;
5053 rgba[base] = (pixel[0].clamp(0.0, 1.0) * 255.0).round() as u8;
5054 rgba[base + 1] = (pixel[1].clamp(0.0, 1.0) * 255.0).round() as u8;
5055 rgba[base + 2] = (pixel[2].clamp(0.0, 1.0) * 255.0).round() as u8;
5056 rgba[base + 3] = (pixel[3].clamp(0.0, 1.0) * 255.0).round() as u8;
5057 }
5058 ImageBitmap::from_rgba8(width, height, rgba).expect("reference dilation image")
5059 }
5060
5061 fn test_font() -> ab_glyph::FontRef<'static> {
5062 ab_glyph::FontRef::try_from_slice(include_bytes!("../assets/NotoSansMerged.ttf"))
5063 .expect("font")
5064 }
5065
5066 fn test_software_font() -> SoftwareTextFont {
5067 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansMerged.ttf").to_vec())
5068 .expect("font")
5069 }
5070
5071 #[test]
5072 fn software_text_font_rejects_invalid_bytes() {
5073 assert!(SoftwareTextFont::from_bytes(vec![0, 1, 2, 3]).is_err());
5074 }
5075
5076 #[test]
5077 fn default_software_text_font_has_no_process_global_cache() {
5078 let source = include_str!("software_text_raster.rs");
5079 let once_lock = ["Once", "Lock"].concat();
5080 let cached_default = ["static ", "FONT"].concat();
5081 let default_font_fn = ["fn ", "default_font()"].concat();
5082
5083 assert!(
5084 !source.contains(&cached_default)
5085 && !source.contains(&default_font_fn)
5086 && !source.contains(&once_lock),
5087 "default software text font construction must be explicit renderer/app-owned state, not a process-global cache"
5088 );
5089 }
5090
5091 #[test]
5092 fn software_text_measurer_empty_font_set_uses_deterministic_fallback_without_panicking() {
5093 let measurer = SoftwareTextMeasurer::from_font_set(SoftwareTextFontSet::empty(), 4);
5094 let style = TextStyle {
5095 span_style: SpanStyle {
5096 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5097 ..Default::default()
5098 },
5099 ..Default::default()
5100 };
5101 let text = AnnotatedString::from("ab\nc");
5102
5103 let metrics = measurer.measure(&text, &style);
5104 assert_eq!(metrics.line_count, 2);
5105 assert!(metrics.width > 0.0);
5106 assert!(metrics.height >= metrics.line_height * 2.0);
5107
5108 let cursor_x = measurer.get_cursor_x_for_offset(&text, &style, 2);
5109 assert!(cursor_x > 0.0);
5110 let second_line_offset =
5111 measurer.get_offset_for_position(&text, &style, 0.0, metrics.line_height);
5112 assert!(
5113 second_line_offset >= "ab\n".len(),
5114 "fallback hit testing should resolve into the second line: {second_line_offset}"
5115 );
5116
5117 let layout = measurer.layout(&text, &style);
5118 assert_eq!(layout.lines.len(), 2);
5119 assert_eq!(layout.glyph_layouts().len(), 3);
5120 }
5121
5122 #[test]
5123 fn software_text_metrics_layout_and_cursor_share_font_backend() {
5124 let font = test_software_font();
5125 let style = TextStyle {
5126 span_style: SpanStyle {
5127 font_size: cranpose_ui::text::TextUnit::Sp(18.0),
5128 ..Default::default()
5129 },
5130 ..Default::default()
5131 };
5132 let text = "Text\nBackend";
5133
5134 let metrics = measure_text_with_font(text, &style, 18.0, &font);
5135 let layout = layout_text_with_font(text, &style, &font);
5136
5137 assert!(metrics.width > 0.0);
5138 assert_eq!(metrics.line_count, 2);
5139 assert_eq!(layout.lines.len(), 2);
5140 assert_eq!(layout.height, metrics.height);
5141 assert!(layout.glyph_layouts().len() >= "TextBackend".len());
5142
5143 let offset =
5144 text_offset_for_position_with_font(text, &style, 0.0, metrics.line_height, &font);
5145 assert!(
5146 offset >= "Text\n".len(),
5147 "second-line hit testing should return a byte offset on the second line: {offset}"
5148 );
5149 let cursor_x = cursor_x_for_offset_with_font(text, &style, "Text".len(), &font);
5150 assert!(cursor_x > 0.0);
5151 }
5152
5153 #[test]
5154 fn software_text_metrics_keep_requested_font_size_for_default_font() {
5155 let font = default_software_text_font().expect("bundled default test font");
5156 let style = TextStyle {
5157 span_style: SpanStyle {
5158 font_size: cranpose_ui::text::TextUnit::Sp(14.0),
5159 ..Default::default()
5160 },
5161 ..Default::default()
5162 };
5163
5164 let metrics = measure_text_with_font("Counter App", &style, 14.0, &font);
5165 assert!(
5166 (metrics.width - 83.16).abs() < 0.05 && (metrics.height - 19.6).abs() < 0.05,
5167 "14sp demo text must use font em metrics, not ab_glyph height units: {metrics:?}"
5168 );
5169 }
5170
5171 #[test]
5172 fn software_text_synthesizes_missing_bold_weight() {
5173 let font = test_software_font();
5174 let normal_style = TextStyle {
5175 span_style: SpanStyle {
5176 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5177 ..Default::default()
5178 },
5179 ..Default::default()
5180 };
5181 let bold_style = TextStyle {
5182 span_style: SpanStyle {
5183 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5184 font_weight: Some(FontWeight::BOLD),
5185 ..Default::default()
5186 },
5187 ..Default::default()
5188 };
5189 let no_synthesis_style = TextStyle {
5190 span_style: SpanStyle {
5191 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5192 font_weight: Some(FontWeight::BOLD),
5193 font_synthesis: Some(FontSynthesis::None),
5194 ..Default::default()
5195 },
5196 ..Default::default()
5197 };
5198
5199 let normal = measure_text_with_font("Save Raster WebP", &normal_style, 20.0, &font);
5200 let synthesized = measure_text_with_font("Save Raster WebP", &bold_style, 20.0, &font);
5201 let disabled = measure_text_with_font("Save Raster WebP", &no_synthesis_style, 20.0, &font);
5202
5203 assert!(
5204 synthesized.width > normal.width * 1.04,
5205 "bold fallback should synthesize heavier advances: normal={normal:?} synthesized={synthesized:?}"
5206 );
5207 assert!(
5208 (disabled.width - normal.width).abs() < 0.01,
5209 "explicit FontSynthesis::None should preserve regular metrics: normal={normal:?} disabled={disabled:?}"
5210 );
5211 }
5212
5213 #[test]
5214 fn rasterized_synthetic_bold_adds_ink_without_changing_line_box() {
5215 let font = test_software_font();
5216 let normal_style = TextStyle {
5217 span_style: SpanStyle {
5218 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5219 ..Default::default()
5220 },
5221 ..Default::default()
5222 };
5223 let bold_style = TextStyle {
5224 span_style: SpanStyle {
5225 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5226 font_weight: Some(FontWeight::BOLD),
5227 ..Default::default()
5228 },
5229 ..Default::default()
5230 };
5231 let normal_metrics = measure_text_with_font("Composer", &normal_style, 20.0, &font);
5232 let bold_metrics = measure_text_with_font("Composer", &bold_style, 20.0, &font);
5233
5234 let normal = rasterize_text_to_image(
5235 "Composer",
5236 Rect {
5237 x: 0.0,
5238 y: 0.0,
5239 width: normal_metrics.width.ceil(),
5240 height: normal_metrics.height.ceil(),
5241 },
5242 &normal_style,
5243 Color::WHITE,
5244 20.0,
5245 1.0,
5246 &font,
5247 )
5248 .expect("normal text image");
5249 let bold = rasterize_text_to_image(
5250 "Composer",
5251 Rect {
5252 x: 0.0,
5253 y: 0.0,
5254 width: bold_metrics.width.ceil(),
5255 height: bold_metrics.height.ceil(),
5256 },
5257 &bold_style,
5258 Color::WHITE,
5259 20.0,
5260 1.0,
5261 &font,
5262 )
5263 .expect("bold text image");
5264
5265 assert_eq!(bold.height(), normal.height());
5266 assert!(
5267 count_ink_pixels(&bold) > count_ink_pixels(&normal),
5268 "synthetic bold should increase rasterized ink coverage"
5269 );
5270 }
5271
5272 #[test]
5273 fn software_text_synthesizes_missing_italic_style() {
5274 let font = test_software_font();
5275 let normal_style = TextStyle {
5276 span_style: SpanStyle {
5277 font_size: cranpose_ui::text::TextUnit::Sp(36.0),
5278 ..Default::default()
5279 },
5280 ..Default::default()
5281 };
5282 let italic_style = TextStyle {
5283 span_style: SpanStyle {
5284 font_size: cranpose_ui::text::TextUnit::Sp(36.0),
5285 font_style: Some(FontStyle::Italic),
5286 ..Default::default()
5287 },
5288 ..Default::default()
5289 };
5290 let no_synthesis_style = TextStyle {
5291 span_style: SpanStyle {
5292 font_size: cranpose_ui::text::TextUnit::Sp(36.0),
5293 font_style: Some(FontStyle::Italic),
5294 font_synthesis: Some(FontSynthesis::None),
5295 ..Default::default()
5296 },
5297 ..Default::default()
5298 };
5299
5300 let normal_metrics = measure_text_with_font("Italic", &normal_style, 36.0, &font);
5301 let italic_metrics = measure_text_with_font("Italic", &italic_style, 36.0, &font);
5302 let disabled_metrics = measure_text_with_font("Italic", &no_synthesis_style, 36.0, &font);
5303
5304 assert!(
5305 italic_metrics.width > normal_metrics.width + 6.0,
5306 "italic fallback should reserve slanted visual overhang: normal={normal_metrics:?} italic={italic_metrics:?}"
5307 );
5308 assert!(
5309 (disabled_metrics.width - normal_metrics.width).abs() < 0.01,
5310 "explicit FontSynthesis::None should preserve regular metrics: normal={normal_metrics:?} disabled={disabled_metrics:?}"
5311 );
5312
5313 let normal = rasterize_text_to_image(
5314 "Italic",
5315 Rect {
5316 x: 0.0,
5317 y: 0.0,
5318 width: normal_metrics.width.ceil(),
5319 height: normal_metrics.height.ceil(),
5320 },
5321 &normal_style,
5322 Color::WHITE,
5323 36.0,
5324 1.0,
5325 &font,
5326 )
5327 .expect("normal text image");
5328 let italic = rasterize_text_to_image(
5329 "Italic",
5330 Rect {
5331 x: 0.0,
5332 y: 0.0,
5333 width: italic_metrics.width.ceil(),
5334 height: italic_metrics.height.ceil(),
5335 },
5336 &italic_style,
5337 Color::WHITE,
5338 36.0,
5339 1.0,
5340 &font,
5341 )
5342 .expect("italic text image");
5343 let disabled = rasterize_text_to_image(
5344 "Italic",
5345 Rect {
5346 x: 0.0,
5347 y: 0.0,
5348 width: disabled_metrics.width.ceil(),
5349 height: disabled_metrics.height.ceil(),
5350 },
5351 &no_synthesis_style,
5352 Color::WHITE,
5353 36.0,
5354 1.0,
5355 &font,
5356 )
5357 .expect("disabled italic text image");
5358
5359 assert_eq!(
5360 normal.pixels(),
5361 disabled.pixels(),
5362 "FontSynthesis::None must not synthesize oblique glyphs"
5363 );
5364 assert!(
5365 vertical_slant_delta(&italic) > vertical_slant_delta(&normal) + 2.0,
5366 "synthetic italic should visibly lean top ink to the right"
5367 );
5368 }
5369
5370 #[test]
5371 fn rasterized_default_text_fills_expected_visual_height() {
5372 let font = default_software_text_font().expect("bundled default test font");
5373 let style = TextStyle {
5374 span_style: SpanStyle {
5375 font_size: cranpose_ui::text::TextUnit::Sp(14.0),
5376 ..Default::default()
5377 },
5378 ..Default::default()
5379 };
5380 let metrics = measure_text_with_font("Counter App", &style, 14.0, &font);
5381 let image = rasterize_text_to_image(
5382 "Counter App",
5383 Rect {
5384 x: 0.0,
5385 y: 0.0,
5386 width: metrics.width.ceil(),
5387 height: metrics.height.ceil(),
5388 },
5389 &style,
5390 Color::WHITE,
5391 14.0,
5392 1.0,
5393 &font,
5394 )
5395 .expect("text image");
5396 let (top, bottom) = ink_y_range(&image).expect("text should contain ink");
5397 let ink_height = bottom - top;
5398
5399 assert!(
5400 ink_height >= 13,
5401 "14sp default text ink should keep visual height parity with the WGPU baseline: top={top} bottom={bottom} image={}x{}",
5402 image.width(),
5403 image.height()
5404 );
5405 }
5406
5407 #[test]
5408 fn software_text_font_selection_preserves_first_complete_default_face() {
5409 let regular =
5410 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansMerged.ttf").to_vec())
5411 .expect("regular test font should load");
5412 let font = software_text_font_from_fonts_or_default(&[
5413 include_bytes!("../assets/NotoSansMerged.ttf"),
5414 include_bytes!("../assets/NotoSansBold.ttf"),
5415 include_bytes!("../assets/TwemojiMozilla.ttf"),
5416 ])
5417 .expect("font selection should resolve a test font");
5418 let style = TextStyle {
5419 span_style: SpanStyle {
5420 font_size: cranpose_ui::text::TextUnit::Sp(18.0),
5421 ..Default::default()
5422 },
5423 ..Default::default()
5424 };
5425
5426 let regular_metrics = measure_text_with_font("UNDER", &style, 18.0, ®ular);
5427 let metrics = measure_text_with_font("UNDER", &style, 18.0, &font);
5428 assert!(
5429 (metrics.width - regular_metrics.width).abs() < 0.01,
5430 "font selection should keep the declared regular face for default text: selected={metrics:?}, regular={regular_metrics:?}"
5431 );
5432 }
5433
5434 #[test]
5435 fn software_text_font_resolution_reuses_cached_font_score() {
5436 let font = test_software_font();
5437 assert!(
5438 font.score.is_complete_default_face(),
5439 "test font should cache complete Latin coverage at load time: supported={} width={}",
5440 font.score.supported_latin_chars,
5441 font.score.latin_sample_width
5442 );
5443
5444 let fonts = SoftwareTextFontSet::from_font(font.clone());
5445 let resolved = fonts
5446 .resolve(&TextStyle {
5447 span_style: SpanStyle {
5448 font_weight: Some(FontWeight::BOLD),
5449 ..Default::default()
5450 },
5451 ..Default::default()
5452 })
5453 .expect("font set should resolve a test font");
5454
5455 assert_eq!(
5456 resolved.score.supported_latin_chars,
5457 font.score.supported_latin_chars
5458 );
5459 assert_eq!(
5460 resolved.score.latin_sample_width,
5461 font.score.latin_sample_width
5462 );
5463 }
5464
5465 #[test]
5466 fn software_text_font_set_resolves_requested_weight() {
5467 let fonts = software_text_font_set_from_fonts_or_default(&[
5468 include_bytes!("../assets/NotoSansMerged.ttf"),
5469 include_bytes!("../assets/NotoSansBold.ttf"),
5470 include_bytes!("../assets/TwemojiMozilla.ttf"),
5471 ]);
5472 let regular = fonts
5473 .resolve(&TextStyle::default())
5474 .expect("font set should resolve regular test font");
5475 let bold_style = TextStyle {
5476 span_style: SpanStyle {
5477 font_weight: Some(FontWeight::BOLD),
5478 ..Default::default()
5479 },
5480 ..Default::default()
5481 };
5482 let bold = fonts
5483 .resolve(&bold_style)
5484 .expect("font set should resolve bold test font");
5485
5486 assert_eq!(regular.weight(), FontWeight::NORMAL);
5487 assert_eq!(bold.weight(), FontWeight::BOLD);
5488
5489 let regular_metrics =
5490 measure_text_with_font("Counter App", &TextStyle::default(), 18.0, regular);
5491 let bold_metrics = measure_text_with_font("Counter App", &bold_style, 18.0, bold);
5492 assert!(
5493 bold_metrics.width > regular_metrics.width,
5494 "bold face resolution should affect real text metrics: regular={regular_metrics:?} bold={bold_metrics:?}"
5495 );
5496 }
5497
5498 fn registered_face(family: &FontFamily, weight: FontWeight) -> SoftwareTextFont {
5499 SoftwareTextFont::from_registered_bytes(
5500 family,
5501 weight,
5502 FontStyle::Normal,
5503 include_bytes!("../assets/NotoSansMerged.ttf").to_vec(),
5504 )
5505 .expect("registered test face")
5506 }
5507
5508 fn style_naming(family: &FontFamily) -> TextStyle {
5509 TextStyle {
5510 span_style: SpanStyle {
5511 font_family: Some(family.clone()),
5512 ..Default::default()
5513 },
5514 ..Default::default()
5515 }
5516 }
5517
5518 fn unregistered_face() -> SoftwareTextFont {
5519 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansBold.ttf").to_vec())
5520 .expect("unregistered test face")
5521 }
5522
5523 #[test]
5524 fn a_named_family_resolves_the_face_registered_under_it() {
5525 let family = FontFamily::named("Game UI");
5528 let fonts = SoftwareTextFontSet::from_faces(vec![
5529 unregistered_face(),
5530 registered_face(&family, FontWeight::NORMAL),
5531 ]);
5532
5533 let resolved = fonts
5534 .resolve(&style_naming(&family))
5535 .expect("registered face");
5536 assert_eq!(
5537 resolved.registered_family(),
5538 Some(FontFamilyKey::of(&family))
5539 );
5540 }
5541
5542 #[test]
5543 fn a_file_backed_family_never_resolves_a_face_filed_under_another_one() {
5544 let mine = FontFamily::loaded_typeface_path("/fonts/Mine.ttf");
5545 let theirs = FontFamily::loaded_typeface_path("/fonts/Theirs.ttf");
5546 let fallback =
5547 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansMerged.ttf").to_vec())
5548 .expect("fallback test face");
5549 let theirs_face = SoftwareTextFont::from_registered_bytes(
5550 &theirs,
5551 FontWeight::BOLD,
5552 FontStyle::Normal,
5553 include_bytes!("../assets/NotoSansBold.ttf").to_vec(),
5554 )
5555 .expect("registered test face");
5556 let fonts = SoftwareTextFontSet::from_faces(vec![fallback.clone(), theirs_face]);
5557
5558 assert_eq!(
5559 fonts
5560 .resolve(&style_naming(&mine))
5561 .expect("fallback face")
5562 .content_hash(),
5563 fallback.content_hash(),
5564 "an unregistered family must fall back rather than borrow someone else's face"
5565 );
5566 assert_eq!(
5567 fonts
5568 .resolve(&style_naming(&theirs))
5569 .expect("registered face")
5570 .registered_family(),
5571 Some(FontFamilyKey::of(&theirs)),
5572 "the family that was registered still resolves to its own face"
5573 );
5574 }
5575
5576 #[test]
5577 fn a_generic_family_only_constrains_the_set_once_a_face_is_registered_for_it() {
5578 let bold_sans_serif = TextStyle {
5579 span_style: SpanStyle {
5580 font_family: Some(FontFamily::SansSerif),
5581 font_weight: Some(FontWeight::BOLD),
5582 ..Default::default()
5583 },
5584 ..Default::default()
5585 };
5586
5587 let unclaimed = software_text_font_set_from_fonts_or_default(&[
5590 include_bytes!("../assets/NotoSansMerged.ttf"),
5591 include_bytes!("../assets/NotoSansBold.ttf"),
5592 ]);
5593 assert_eq!(
5594 unclaimed
5595 .resolve(&bold_sans_serif)
5596 .expect("bold face")
5597 .weight(),
5598 FontWeight::BOLD
5599 );
5600
5601 let claimed = SoftwareTextFontSet::from_faces(vec![
5604 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansBold.ttf").to_vec())
5605 .expect("bold test face"),
5606 registered_face(&FontFamily::SansSerif, FontWeight::NORMAL),
5607 ]);
5608 let resolved = claimed.resolve(&bold_sans_serif).expect("system face");
5609 assert_eq!(
5610 resolved.registered_family(),
5611 Some(FontFamilyKey::of(&FontFamily::SansSerif))
5612 );
5613 }
5614
5615 #[test]
5616 fn an_app_supplied_family_measures_once_and_is_served_from_the_metrics_cache() {
5617 let family = FontFamily::named("Game UI");
5618 let measurer = SoftwareTextMeasurer::from_font_set(
5619 SoftwareTextFontSet::from_faces(vec![registered_face(&family, FontWeight::NORMAL)]),
5620 64,
5621 );
5622 let style = style_naming(&family);
5623 let text = AnnotatedString::from("SCORE 1234");
5624
5625 let first = measurer.measure(&text, &style);
5626 let stats_after_first = measurer.lock_cache().glyph_metrics.stats();
5627 for _ in 0..60 {
5628 assert_eq!(measurer.measure(&text, &style), first);
5629 }
5630
5631 assert_eq!(
5632 measurer.lock_cache().glyph_metrics.stats(),
5633 stats_after_first,
5634 "repeat frames of an unchanged string must not re-shape against the app face"
5635 );
5636 }
5637
5638 #[test]
5639 fn software_text_metrics_use_largest_annotated_span_font_size() {
5640 let font = default_software_text_font().expect("bundled default test font");
5641 let text = AnnotatedString::builder()
5642 .push_style(SpanStyle {
5643 font_size: cranpose_ui::text::TextUnit::Sp(30.0),
5644 ..Default::default()
5645 })
5646 .append("BIG ")
5647 .pop()
5648 .push_style(SpanStyle {
5649 font_size: cranpose_ui::text::TextUnit::Sp(10.0),
5650 ..Default::default()
5651 })
5652 .append("small")
5653 .pop()
5654 .to_annotated_string();
5655
5656 let metrics = measure_annotated_text_with_font(&text, &TextStyle::default(), 14.0, &font);
5657
5658 assert!(
5659 metrics.height >= 30.0,
5660 "rich text metrics must include the largest span height: {metrics:?}"
5661 );
5662 assert!(
5663 metrics.width > 48.0,
5664 "rich text metrics should measure run widths at their span sizes: {metrics:?}"
5665 );
5666 }
5667
5668 #[test]
5669 fn software_text_line_height_matches_full_measurement_without_width_layout() {
5670 let measurer = SoftwareTextMeasurer::new(
5671 default_software_text_font().expect("bundled default test font"),
5672 8,
5673 );
5674 let text = AnnotatedString::builder()
5675 .append("normal ")
5676 .push_style(SpanStyle {
5677 font_size: cranpose_ui::text::TextUnit::Sp(32.0),
5678 ..Default::default()
5679 })
5680 .append("large")
5681 .pop()
5682 .append("\nsecond line")
5683 .to_annotated_string();
5684 let style = TextStyle::default();
5685
5686 let measured = measurer.measure(&text, &style);
5687 let line_height = measurer.line_height(&text, &style);
5688
5689 assert_eq!(line_height, measured.line_height);
5690 assert!(
5691 line_height > measurer.line_height(&AnnotatedString::from("normal"), &style),
5692 "span font size should affect fast line-height lookup"
5693 );
5694 }
5695
5696 #[test]
5697 fn solid_text_atlas_line_advance_matches_measured_line_height() {
5698 let font = default_software_text_font().expect("bundled default test font");
5699 let fonts = SoftwareTextFontSet::from_font(font);
5700 let style = TextStyle::default();
5701 let text = AnnotatedString::from("A\nA\nA\nA");
5702 let font_size = style.resolve_font_size(14.0);
5703 let metrics = measure_annotated_text_with_font_set(&text, &style, font_size, &fonts);
5704 let rect = Rect {
5705 x: 0.0,
5706 y: 0.0,
5707 width: 120.0,
5708 height: metrics.height,
5709 };
5710 let mut glyph_cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(16);
5711 let mut run = Vec::new();
5712
5713 collect_solid_text_atlas_run(
5714 &text,
5715 rect,
5716 &style,
5717 Color(1.0, 1.0, 1.0, 1.0),
5718 font_size,
5719 1.0,
5720 &fonts,
5721 &mut glyph_cache,
5722 &mut run,
5723 )
5724 .expect("atlas-compatible text");
5725
5726 let mut glyph_y: Vec<i32> = run.iter().map(|glyph| glyph.placement().y).collect();
5727 glyph_y.sort_unstable();
5728 glyph_y.dedup();
5729 assert_eq!(glyph_y.len(), 4);
5730 for window in glyph_y.windows(2) {
5731 let advance = (window[1] - window[0]) as f32;
5732 assert!(
5733 (advance - metrics.line_height).abs() <= 1.0,
5734 "glyph advance {advance} should match measured line height {}",
5735 metrics.line_height
5736 );
5737 }
5738 }
5739
5740 #[test]
5741 fn software_text_metrics_cache_keys_include_span_styles() {
5742 let measurer = SoftwareTextMeasurer::new(
5743 default_software_text_font().expect("bundled default test font"),
5744 8,
5745 );
5746 let plain = AnnotatedString::from("BIG small");
5747 let rich = AnnotatedString::builder()
5748 .push_style(SpanStyle {
5749 font_size: cranpose_ui::text::TextUnit::Sp(30.0),
5750 ..Default::default()
5751 })
5752 .append("BIG ")
5753 .pop()
5754 .append("small")
5755 .to_annotated_string();
5756
5757 let plain_metrics = measurer.measure(&plain, &TextStyle::default());
5758 let rich_metrics = measurer.measure(&rich, &TextStyle::default());
5759
5760 assert!(
5761 rich_metrics.height > plain_metrics.height,
5762 "cached plain text metrics must not be reused for styled text: plain={plain_metrics:?} rich={rich_metrics:?}"
5763 );
5764 }
5765
5766 #[test]
5767 fn software_text_metrics_cache_recovers_after_poison() {
5768 let measurer = SoftwareTextMeasurer::new(
5769 default_software_text_font().expect("bundled default test font"),
5770 8,
5771 );
5772 let text = AnnotatedString::from("Recovered text metrics");
5773
5774 let poison_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5775 let _guard = measurer
5776 .cache
5777 .lock()
5778 .unwrap_or_else(|poisoned| poisoned.into_inner());
5779 panic!("poison software text metrics cache for recovery test");
5780 }));
5781
5782 assert!(poison_result.is_err());
5783
5784 let metrics = measurer.measure(&text, &TextStyle::default());
5785 assert!(metrics.width > 0.0);
5786 assert!(metrics.height > 0.0);
5787
5788 let subset =
5789 measurer.measure_subsequence(&text, 0.."Recovered".len(), &TextStyle::default());
5790 assert!(subset.width > 0.0);
5791 assert!(subset.width < metrics.width);
5792 }
5793
5794 #[test]
5795 fn software_text_prefix_widths_match_subsequence_measurement() {
5796 let measurer = SoftwareTextMeasurer::new(
5797 default_software_text_font().expect("bundled default test font"),
5798 8,
5799 );
5800 let style = TextStyle {
5801 span_style: SpanStyle {
5802 font_size: cranpose_ui::text::TextUnit::Sp(18.0),
5803 ..Default::default()
5804 },
5805 ..Default::default()
5806 };
5807 let text = AnnotatedString::from("Hello Prefix Widths");
5808 let widths = measurer
5809 .measure_line_prefix_widths(&text, 0..text.text.len(), &style)
5810 .expect("uniform line should expose prefix widths");
5811
5812 let start = "Hello ".len();
5813 let end = "Hello Prefix".len();
5814 let expected = measurer
5815 .measure_subsequence(&text, start..end, &style)
5816 .width;
5817 let actual = widths
5818 .width_for_char_range(6, 12)
5819 .expect("valid char range");
5820
5821 assert!(
5822 (actual - expected).abs() < 0.01,
5823 "prefix width should match exact subsequence width: actual={actual}, expected={expected}"
5824 );
5825 }
5826
5827 #[test]
5828 fn software_text_line_width_and_prefix_width_share_cached_plan() {
5829 let measurer = SoftwareTextMeasurer::new(
5830 default_software_text_font().expect("bundled default test font"),
5831 8,
5832 );
5833 let style = TextStyle::default();
5834 let text = AnnotatedString::from("shared prefix plan ".repeat(32).as_str());
5835 let line_range = 0..text.text.len();
5836
5837 let width = measurer
5838 .measure_line_width(&text, line_range.clone(), &style)
5839 .expect("software text should expose a line width");
5840 let stats_after_width = {
5841 let cache = measurer.lock_cache();
5842 assert_eq!(cache.line_prefix_widths.len(), 1);
5843 cache.glyph_metrics.stats()
5844 };
5845
5846 let widths = measurer
5847 .measure_line_prefix_widths(&text, line_range, &style)
5848 .expect("line width probe should cache the prefix plan");
5849 let stats_after_prefix = measurer.lock_cache().glyph_metrics.stats();
5850
5851 assert_eq!(stats_after_prefix, stats_after_width);
5852 assert!(
5853 (width - widths.width_for_char_range(0, widths.char_count()).unwrap()).abs() < 0.01,
5854 "cached line-width probe and prefix plan must agree"
5855 );
5856 }
5857
5858 #[test]
5859 fn software_text_glyph_metrics_cache_reuses_common_glyphs_across_unique_lines() {
5860 let measurer = SoftwareTextMeasurer::new(
5861 default_software_text_font().expect("bundled default test font"),
5862 8,
5863 );
5864 let style = TextStyle::default();
5865 let first = AnnotatedString::from("algorithm data structure ".repeat(24).as_str());
5866 let second =
5867 AnnotatedString::from("algorithmic structures repeat data ".repeat(24).as_str());
5868
5869 measurer
5870 .measure_line_prefix_widths(&first, 0..first.text.len(), &style)
5871 .expect("first unique line should measure");
5872 let stats_after_first = measurer.lock_cache().glyph_metrics.stats();
5873
5874 measurer
5875 .measure_line_prefix_widths(&second, 0..second.text.len(), &style)
5876 .expect("second unique line should measure");
5877 let stats_after_second = measurer.lock_cache().glyph_metrics.stats();
5878
5879 assert!(
5880 stats_after_second.glyph_hits > stats_after_first.glyph_hits,
5881 "unique markdown rows should reuse retained glyph metrics: first={stats_after_first:?} second={stats_after_second:?}"
5882 );
5883 assert!(
5884 stats_after_second.kern_hits > stats_after_first.kern_hits,
5885 "unique markdown rows should reuse retained kerning metrics: first={stats_after_first:?} second={stats_after_second:?}"
5886 );
5887 }
5888
5889 #[test]
5890 fn rasterized_gradient_text_shows_color_transition() {
5891 let font = test_font();
5892 let plain_style = TextStyle::default();
5895 let probe = rasterize_text_to_image_with_font(
5896 "MMMMMMMM",
5897 Rect {
5898 x: 0.0,
5899 y: 0.0,
5900 width: 320.0,
5901 height: 96.0,
5902 },
5903 &plain_style,
5904 Color::WHITE,
5905 48.0,
5906 1.0,
5907 &font,
5908 )
5909 .expect("probe image");
5910 let (ink_x_min, ink_x_max) = ink_x_range(&probe).expect("probe must contain ink");
5911 let gradient_end = ink_x_max as f32;
5912
5913 let style = TextStyle {
5914 span_style: SpanStyle {
5915 brush: Some(Brush::linear_gradient_range(
5916 vec![Color::RED, Color::BLUE],
5917 Point::new(0.0, 0.0),
5918 Point::new(gradient_end, 0.0),
5919 )),
5920 ..Default::default()
5921 },
5922 ..Default::default()
5923 };
5924
5925 let image = rasterize_text_to_image_with_font(
5926 "MMMMMMMM",
5927 Rect {
5928 x: 0.0,
5929 y: 0.0,
5930 width: 320.0,
5931 height: 96.0,
5932 },
5933 &style,
5934 Color::WHITE,
5935 48.0,
5936 1.0,
5937 &font,
5938 )
5939 .expect("rasterized image");
5940
5941 let ink_span = ink_x_max.saturating_sub(ink_x_min).max(1);
5942 let left_end = ink_x_min + ink_span * 3 / 10;
5943 let right_start = ink_x_max.saturating_sub(ink_span * 3 / 10);
5944 let left = average_ink_rgb(&image, ink_x_min, left_end, 8, 90).expect("left ink");
5945 let right = average_ink_rgb(&image, right_start, ink_x_max, 8, 90).expect("right ink");
5946 assert!(
5947 left[0] > left[2] * 1.1,
5948 "left region should be red dominant, got {left:?}"
5949 );
5950 assert!(
5951 right[2] > right[0] * 1.1,
5952 "right region should be blue dominant, got {right:?}"
5953 );
5954 }
5955
5956 #[test]
5957 fn rasterized_stroke_and_fill_ink_coverage_differs() {
5958 let font = test_font();
5959 let fill_style = TextStyle::default();
5960 let stroke_style = TextStyle {
5961 span_style: SpanStyle {
5962 draw_style: Some(TextDrawStyle::Stroke { width: 6.0 }),
5963 ..Default::default()
5964 },
5965 ..Default::default()
5966 };
5967 let rect = Rect {
5968 x: 0.0,
5969 y: 0.0,
5970 width: 320.0,
5971 height: 96.0,
5972 };
5973
5974 let fill = rasterize_text_to_image_with_font(
5975 "MMMMMMMM",
5976 rect,
5977 &fill_style,
5978 Color::WHITE,
5979 48.0,
5980 1.0,
5981 &font,
5982 )
5983 .expect("fill image");
5984 let stroke = rasterize_text_to_image_with_font(
5985 "MMMMMMMM",
5986 rect,
5987 &stroke_style,
5988 Color::WHITE,
5989 48.0,
5990 1.0,
5991 &font,
5992 )
5993 .expect("stroke image");
5994
5995 let fill_ink = count_ink_pixels(&fill);
5996 let stroke_ink = count_ink_pixels(&stroke);
5997 assert_ne!(fill.pixels(), stroke.pixels());
5998 assert!(
5999 fill_ink.abs_diff(stroke_ink) > 300,
6000 "fill/stroke ink coverage should differ; fill={fill_ink}, stroke={stroke_ink}"
6001 );
6002 }
6003
6004 #[test]
6005 fn stroke_path_uses_miter_join_for_acute_apexes() {
6006 let font = test_font();
6007 let fill_style = TextStyle::default();
6008 let stroke_width = 12.0;
6009 let stroke_style = TextStyle {
6010 span_style: SpanStyle {
6011 draw_style: Some(TextDrawStyle::Stroke {
6012 width: stroke_width,
6013 }),
6014 ..Default::default()
6015 },
6016 ..Default::default()
6017 };
6018 let rect = Rect {
6019 x: 0.0,
6020 y: 0.0,
6021 width: 180.0,
6022 height: 140.0,
6023 };
6024
6025 let fill = rasterize_text_to_image_with_font(
6026 "A",
6027 rect,
6028 &fill_style,
6029 Color::WHITE,
6030 110.0,
6031 1.0,
6032 &font,
6033 )
6034 .expect("fill image");
6035 let stroke = rasterize_text_to_image_with_font(
6036 "A",
6037 rect,
6038 &stroke_style,
6039 Color::WHITE,
6040 110.0,
6041 1.0,
6042 &font,
6043 )
6044 .expect("stroke image");
6045
6046 let fill_top = top_ink_row(&fill).expect("fill top row");
6047 let stroke_top = top_ink_row(&stroke).expect("stroke top row");
6048 let reference_dilation =
6049 rasterize_reference_dilation_stroke("A", rect, 110.0, stroke_width, &font);
6050 let reference_top = top_ink_row(&reference_dilation).expect("reference top row");
6051 let extra_extension = fill_top.saturating_sub(stroke_top) as f32;
6052 let half_stroke = stroke_width * 0.5;
6053 assert!(
6054 extra_extension >= half_stroke - 0.25,
6055 "stroke apex should extend by roughly at least half stroke width; fill_top={fill_top}, stroke_top={stroke_top}, half_stroke={half_stroke:.2}"
6056 );
6057 assert!(
6058 stroke.pixels() != reference_dilation.pixels(),
6059 "path stroke should diverge from mask-dilation reference output"
6060 );
6061 assert!(
6062 stroke_top <= reference_top,
6063 "miter stroke should keep acute apex at least as extended as mask-dilation reference; stroke_top={stroke_top}, reference_top={reference_top}"
6064 );
6065 }
6066
6067 #[test]
6068 fn shadow_blur_radius_changes_spread_for_shared_raster_path() {
6069 let font = test_font();
6070 let base_shadow = Shadow {
6071 color: Color(0.0, 0.0, 0.0, 0.9),
6072 offset: Point::new(5.5, 4.25),
6073 blur_radius: 0.0,
6074 };
6075 let hard_shadow_style = TextStyle {
6076 span_style: SpanStyle {
6077 shadow: Some(base_shadow),
6078 ..Default::default()
6079 },
6080 ..Default::default()
6081 };
6082 let blurred_shadow_style = TextStyle {
6083 span_style: SpanStyle {
6084 shadow: Some(Shadow {
6085 blur_radius: 9.0,
6086 ..base_shadow
6087 }),
6088 ..Default::default()
6089 },
6090 ..Default::default()
6091 };
6092 let rect = Rect {
6093 x: 0.0,
6094 y: 0.0,
6095 width: 320.0,
6096 height: 120.0,
6097 };
6098
6099 let hard_shadow = rasterize_text_to_image_with_font(
6100 "Shared shadow",
6101 rect,
6102 &hard_shadow_style,
6103 Color::TRANSPARENT,
6104 48.0,
6105 1.0,
6106 &font,
6107 )
6108 .expect("hard shadow image");
6109 let blurred_shadow = rasterize_text_to_image_with_font(
6110 "Shared shadow",
6111 rect,
6112 &blurred_shadow_style,
6113 Color::TRANSPARENT,
6114 48.0,
6115 1.0,
6116 &font,
6117 )
6118 .expect("blurred shadow image");
6119
6120 let hard_ink = count_ink_pixels(&hard_shadow);
6121 let blurred_ink = count_ink_pixels(&blurred_shadow);
6122 assert_ne!(
6123 hard_shadow.pixels(),
6124 blurred_shadow.pixels(),
6125 "blur radius should change rasterized shadow output"
6126 );
6127 assert!(
6128 blurred_ink > hard_ink,
6129 "blurred shadow should spread to more pixels; hard={hard_ink}, blurred={blurred_ink}"
6130 );
6131 }
6132
6133 #[test]
6134 fn text_motion_changes_fractional_shadow_sampling() {
6135 let font = test_font();
6136 let base_shadow = Shadow {
6137 color: Color(0.0, 0.0, 0.0, 0.9),
6138 offset: Point::new(3.35, 2.65),
6139 blur_radius: 6.0,
6140 };
6141 let static_style = TextStyle {
6142 span_style: SpanStyle {
6143 shadow: Some(base_shadow),
6144 ..Default::default()
6145 },
6146 paragraph_style: cranpose_ui::text::ParagraphStyle {
6147 text_motion: Some(TextMotion::Static),
6148 ..Default::default()
6149 },
6150 };
6151 let animated_style = TextStyle {
6152 span_style: SpanStyle {
6153 shadow: Some(base_shadow),
6154 ..Default::default()
6155 },
6156 paragraph_style: cranpose_ui::text::ParagraphStyle {
6157 text_motion: Some(TextMotion::Animated),
6158 ..Default::default()
6159 },
6160 };
6161 let rect = Rect {
6162 x: 11.35,
6163 y: 7.65,
6164 width: 280.0,
6165 height: 120.0,
6166 };
6167
6168 let static_image = rasterize_text_to_image_with_font(
6169 "Motion shadow",
6170 rect,
6171 &static_style,
6172 Color::TRANSPARENT,
6173 42.0,
6174 1.0,
6175 &font,
6176 )
6177 .expect("static image");
6178 let animated_image = rasterize_text_to_image_with_font(
6179 "Motion shadow",
6180 rect,
6181 &animated_style,
6182 Color::TRANSPARENT,
6183 42.0,
6184 1.0,
6185 &font,
6186 )
6187 .expect("animated image");
6188
6189 assert_ne!(
6190 static_image.pixels(),
6191 animated_image.pixels(),
6192 "TextMotion::Static should quantize shadow placement while Animated keeps fractional sampling"
6193 );
6194 }
6195
6196 #[test]
6197 fn static_text_motion_aligns_glyph_positions_to_pixel_grid() {
6198 let font = test_font();
6199 let base_glyph = layout_line_glyphs(&font, "A", 17.0, point(0.0, 13.37))
6200 .into_iter()
6201 .next()
6202 .expect("glyph");
6203 let static_aligned = align_glyph_for_text_motion(base_glyph, true);
6204 let static_position = static_aligned.position;
6205 assert!(
6206 (static_position.x - static_position.x.round()).abs() < f32::EPSILON,
6207 "static text should snap glyph x to pixel grid"
6208 );
6209 assert!(
6210 (static_position.y - static_position.y.round()).abs() < f32::EPSILON,
6211 "static text should snap glyph y to pixel grid"
6212 );
6213
6214 let animated_source = layout_line_glyphs(&font, "A", 17.0, point(0.0, 13.37))
6215 .into_iter()
6216 .next()
6217 .expect("glyph");
6218 let animated_aligned = align_glyph_for_text_motion(animated_source, false);
6219 let animated_position = animated_aligned.position;
6220 assert!(
6221 (animated_position.y - 13.37).abs() < 1e-3,
6222 "animated text should preserve fractional glyph position"
6223 );
6224 }
6225}