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)]
693struct CachedGlyphMetrics {
694 glyph_id: GlyphId,
695 advance_unscaled: f32,
696}
697
698#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
699struct GlyphMetricsKey {
700 font_hash: u64,
701 ch: char,
702}
703
704#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
705struct KernMetricsKey {
706 font_hash: u64,
707 previous_id: u32,
708 glyph_id: u32,
709}
710
711#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
712struct SoftwareTextGlyphMetricsStats {
713 glyph_hits: u64,
714 glyph_misses: u64,
715 kern_hits: u64,
716 kern_misses: u64,
717}
718
719struct SoftwareTextGlyphMetricsCache {
720 glyphs: BoundedLruCache<GlyphMetricsKey, CachedGlyphMetrics>,
721 kerns: BoundedLruCache<KernMetricsKey, f32>,
722 stats: SoftwareTextGlyphMetricsStats,
723}
724
725impl SoftwareTextGlyphMetricsCache {
726 fn new() -> Self {
727 Self {
728 glyphs: BoundedLruCache::with_capacity_at_least_one(
729 SOFTWARE_TEXT_GLYPH_METRICS_CACHE_CAPACITY,
730 ),
731 kerns: BoundedLruCache::with_capacity_at_least_one(
732 SOFTWARE_TEXT_KERN_METRICS_CACHE_CAPACITY,
733 ),
734 stats: SoftwareTextGlyphMetricsStats::default(),
735 }
736 }
737
738 #[cfg(test)]
739 fn stats(&self) -> SoftwareTextGlyphMetricsStats {
740 self.stats
741 }
742
743 fn glyph_metrics<F, S>(
746 &mut self,
747 font: &SoftwareTextFont,
748 scaled_font: &S,
749 ch: char,
750 ) -> CachedGlyphMetrics
751 where
752 F: Font,
753 S: ScaleFont<F>,
754 {
755 let key = GlyphMetricsKey {
756 font_hash: font.content_hash(),
757 ch,
758 };
759 if let Some(metrics) = self.glyphs.get(&key).copied() {
760 self.stats.glyph_hits = self.stats.glyph_hits.saturating_add(1);
761 return metrics;
762 }
763
764 let glyph_id = scaled_font.font().glyph_id(ch);
765 let metrics = CachedGlyphMetrics {
766 glyph_id,
767 advance_unscaled: scaled_font.font().h_advance_unscaled(glyph_id).max(0.0),
768 };
769 self.glyphs.put(key, metrics);
770 self.stats.glyph_misses = self.stats.glyph_misses.saturating_add(1);
771 metrics
772 }
773
774 fn kern<F, S>(
777 &mut self,
778 font: &SoftwareTextFont,
779 scaled_font: &S,
780 previous_id: GlyphId,
781 glyph_id: GlyphId,
782 ) -> f32
783 where
784 F: Font,
785 S: ScaleFont<F>,
786 {
787 let key = KernMetricsKey {
788 font_hash: font.content_hash(),
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.font().kern_unscaled(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 h_scale = scaled_font.h_scale_factor();
2960 let mut width = 0.0f32;
2961 let mut previous = None;
2962
2963 for ch in text.chars() {
2964 let metrics = glyph_metrics.glyph_metrics(font, &scaled_font, ch);
2965 if let Some(previous_id) = previous {
2966 width +=
2967 glyph_metrics.kern(font, &scaled_font, previous_id, metrics.glyph_id) * h_scale;
2968 }
2969 width += metrics.advance_unscaled * h_scale;
2970 previous = Some(metrics.glyph_id);
2971 }
2972
2973 width.max(0.0)
2974}
2975
2976fn annotated_line_prefix_widths_with_font_set_cached(
2977 text: &AnnotatedString,
2978 line_range: std::ops::Range<usize>,
2979 style: &TextStyle,
2980 fonts: &SoftwareTextFontSet,
2981 cache: &mut SoftwareTextMetricsCache,
2982) -> Option<TextLinePrefixWidths> {
2983 let mut boundaries = text.span_boundaries();
2984 boundaries.push(line_range.start);
2985 boundaries.push(line_range.end);
2986 boundaries.sort_unstable();
2987 boundaries.dedup();
2988 boundaries.retain(|offset| {
2989 *offset >= line_range.start
2990 && *offset <= line_range.end
2991 && text.text.is_char_boundary(*offset)
2992 });
2993
2994 let char_count = text.text[line_range.clone()].chars().count();
2995 let mut prefix_widths = Vec::with_capacity(char_count + 1);
2996 let mut separator_before = Vec::with_capacity(char_count);
2997 let non_empty_overhang = {
2998 let mut sink = PrefixWidthSegmentSink {
2999 prefix_widths: &mut prefix_widths,
3000 separator_before: &mut separator_before,
3001 width: 0.0,
3002 non_empty_overhang: 0.0,
3003 };
3004 sink.prefix_widths.push(sink.width);
3005
3006 for range in boundaries.windows(2) {
3007 let start = range[0];
3008 let end = range[1];
3009 if start >= end {
3010 continue;
3011 }
3012 let segment = &text.text[start..end];
3013 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
3014 append_prefix_width_segment_cached(segment, &segment_style, fonts, cache, &mut sink);
3015 }
3016
3017 sink.non_empty_overhang
3018 };
3019
3020 TextLinePrefixWidths::from_parts(prefix_widths, separator_before, non_empty_overhang)
3021}
3022
3023struct PrefixWidthSegmentSink<'a> {
3024 prefix_widths: &'a mut Vec<f32>,
3025 separator_before: &'a mut Vec<f32>,
3026 width: f32,
3027 non_empty_overhang: f32,
3028}
3029
3030fn append_prefix_width_segment_cached(
3031 segment: &str,
3032 style: &TextStyle,
3033 fonts: &SoftwareTextFontSet,
3034 cache: &mut SoftwareTextMetricsCache,
3035 sink: &mut PrefixWidthSegmentSink<'_>,
3036) {
3037 if segment.is_empty() {
3038 return;
3039 }
3040
3041 let font_size = resolve_font_size(style);
3042 if let Some(font) = fonts.resolve(style) {
3043 append_font_prefix_width_segment_cached(segment, style, font_size, font, cache, sink);
3044 } else {
3045 append_fallback_prefix_width_segment(segment, style, font_size, sink);
3046 }
3047}
3048
3049fn append_font_prefix_width_segment_cached(
3050 segment: &str,
3051 style: &TextStyle,
3052 font_size: f32,
3053 font: &SoftwareTextFont,
3054 cache: &mut SoftwareTextMetricsCache,
3055 sink: &mut PrefixWidthSegmentSink<'_>,
3056) {
3057 let glyph_font_size = font.ab_glyph_px_size(font_size);
3058 let scaled_font = font.font.as_scaled(PxScale::from(glyph_font_size));
3059 let letter_spacing = resolve_letter_spacing(style, font_size);
3060 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, 1.0);
3061 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0);
3062 sink.non_empty_overhang = sink
3063 .non_empty_overhang
3064 .max(style_synthesis.visual_overhang_px());
3065
3066 let mut previous = None;
3067 let h_scale = scaled_font.h_scale_factor();
3069
3070 for (index, ch) in segment.chars().enumerate() {
3071 let metrics = cache.glyph_metrics.glyph_metrics(font, &scaled_font, ch);
3072 let separator = if index == 0 {
3073 0.0
3074 } else {
3075 previous
3076 .map(|previous_id| {
3077 weight_synthesis.apply_width(
3078 cache
3079 .glyph_metrics
3080 .kern(font, &scaled_font, previous_id, metrics.glyph_id)
3081 * h_scale,
3082 )
3083 })
3084 .unwrap_or(0.0)
3085 + letter_spacing
3086 };
3087 sink.separator_before.push(separator);
3088 sink.width += separator + weight_synthesis.apply_width(metrics.advance_unscaled * h_scale);
3089 sink.prefix_widths.push(sink.width.max(0.0));
3090 previous = Some(metrics.glyph_id);
3091 }
3092}
3093
3094fn append_fallback_prefix_width_segment(
3095 segment: &str,
3096 style: &TextStyle,
3097 font_size: f32,
3098 sink: &mut PrefixWidthSegmentSink<'_>,
3099) {
3100 let char_width = fallback_char_width(font_size);
3101 let letter_spacing = resolve_letter_spacing(style, font_size);
3102 for (index, _) in segment.chars().enumerate() {
3103 let separator = if index == 0 { 0.0 } else { letter_spacing };
3104 sink.separator_before.push(separator);
3105 sink.width += separator + char_width;
3106 sink.prefix_widths.push(sink.width.max(0.0));
3107 }
3108}
3109
3110fn byte_offset_for_char_index(text: &str, char_index: usize) -> usize {
3111 text.char_indices()
3112 .map(|(index, _)| index)
3113 .nth(char_index)
3114 .unwrap_or(text.len())
3115}
3116
3117fn measure_text_impl(
3118 text: &str,
3119 style: &TextStyle,
3120 font_size: f32,
3121 glyph_font_size: f32,
3122 font: &impl Font,
3123 resolved_style: FontStyle,
3124 resolved_weight: FontWeight,
3125) -> TextMetrics {
3126 let line_height = resolve_line_height(style, font_size * 1.4);
3127 let letter_spacing = resolve_letter_spacing(style, font_size);
3128 let weight_synthesis = TextWeightSynthesis::for_style(style, resolved_weight, font_size, 1.0);
3129 let style_synthesis = TextStyleSynthesis::for_style(style, resolved_style, font_size, 1.0);
3130
3131 let lines: Vec<&str> = text.split('\n').collect();
3132 let line_count = lines.len().max(1);
3133
3134 let mut max_width: f32 = 0.0;
3135 for line in &lines {
3136 let line_width = line_advance_width(font, line, glyph_font_size);
3137 let char_spacing = (line.chars().count().saturating_sub(1) as f32) * letter_spacing;
3138 let line_width = (weight_synthesis.apply_width(line_width) + char_spacing).max(0.0);
3139 let line_width = if line.is_empty() {
3140 line_width
3141 } else {
3142 line_width + style_synthesis.visual_overhang_px()
3143 };
3144 max_width = max_width.max(line_width);
3145 }
3146
3147 TextMetrics {
3148 width: max_width,
3149 height: line_count as f32 * line_height,
3150 line_height,
3151 line_count,
3152 }
3153}
3154
3155fn measure_text_impl_cached(
3156 text: &str,
3157 style: &TextStyle,
3158 font_size: f32,
3159 font: &SoftwareTextFont,
3160 cache: &mut SoftwareTextMetricsCache,
3161) -> TextMetrics {
3162 let line_height = resolve_line_height(style, font_size * 1.4);
3163 let letter_spacing = resolve_letter_spacing(style, font_size);
3164 let weight_synthesis = TextWeightSynthesis::for_style(style, font.weight(), font_size, 1.0);
3165 let style_synthesis = TextStyleSynthesis::for_style(style, font.style(), font_size, 1.0);
3166 let glyph_font_size = font.ab_glyph_px_size(font_size);
3167
3168 let lines: Vec<&str> = text.split('\n').collect();
3169 let line_count = lines.len().max(1);
3170
3171 let mut max_width: f32 = 0.0;
3172 for line in &lines {
3173 let line_width =
3174 cached_line_advance_width(font, line, glyph_font_size, &mut cache.glyph_metrics);
3175 let char_spacing = (line.chars().count().saturating_sub(1) as f32) * letter_spacing;
3176 let line_width = (weight_synthesis.apply_width(line_width) + char_spacing).max(0.0);
3177 let line_width = if line.is_empty() {
3178 line_width
3179 } else {
3180 line_width + style_synthesis.visual_overhang_px()
3181 };
3182 max_width = max_width.max(line_width);
3183 }
3184
3185 TextMetrics {
3186 width: max_width,
3187 height: line_count as f32 * line_height,
3188 line_height,
3189 line_count,
3190 }
3191}
3192
3193fn measure_annotated_text_with_resolver(
3194 text: &AnnotatedString,
3195 style: &TextStyle,
3196 font_size: f32,
3197 fonts: &SoftwareTextFontSet,
3198 mut cache: Option<&mut SoftwareTextMetricsCache>,
3199) -> TextMetrics {
3200 let Some(base_font) = fonts.resolve(style) else {
3201 return fallback_text_metrics(text.text.as_str(), style, font_size);
3202 };
3203 let base_line_height = line_height_for_style(style, font_size, &base_font.font);
3204 let mut boundaries = text.span_boundaries();
3205 for (offset, ch) in text.text.char_indices() {
3206 if ch == '\n' {
3207 boundaries.push(offset);
3208 boundaries.push(offset + ch.len_utf8());
3209 }
3210 }
3211 boundaries.sort_unstable();
3212 boundaries.dedup();
3213 boundaries.retain(|offset| *offset <= text.text.len() && text.text.is_char_boundary(*offset));
3214
3215 let mut line_count = 1usize;
3216 let mut max_width = 0.0f32;
3217 let mut current_line_width = 0.0f32;
3218
3219 for range in boundaries.windows(2) {
3220 let start = range[0];
3221 let end = range[1];
3222 if start == end {
3223 continue;
3224 }
3225 let segment = &text.text[start..end];
3226 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
3227 let segment_font_size = resolve_font_size(&segment_style);
3228 let Some(segment_font) = fonts.resolve(&segment_style) else {
3229 let mut remaining = segment;
3230 loop {
3231 if let Some(newline_offset) = remaining.find('\n') {
3232 let before_newline = &remaining[..newline_offset];
3233 if !before_newline.is_empty() {
3234 current_line_width += fallback_text_metrics(
3235 before_newline,
3236 &segment_style,
3237 segment_font_size,
3238 )
3239 .width;
3240 }
3241 max_width = max_width.max(current_line_width);
3242 current_line_width = 0.0;
3243 line_count += 1;
3244 remaining = &remaining[newline_offset + 1..];
3245 if remaining.is_empty() {
3246 break;
3247 }
3248 } else {
3249 if !remaining.is_empty() {
3250 current_line_width +=
3251 fallback_text_metrics(remaining, &segment_style, segment_font_size)
3252 .width;
3253 }
3254 break;
3255 }
3256 }
3257 continue;
3258 };
3259
3260 let mut remaining = segment;
3261 loop {
3262 if let Some(newline_offset) = remaining.find('\n') {
3263 let before_newline = &remaining[..newline_offset];
3264 if !before_newline.is_empty() {
3265 let metrics = if let Some(cache) = cache.as_deref_mut() {
3266 measure_text_with_font_cached(
3267 before_newline,
3268 &segment_style,
3269 segment_font_size,
3270 segment_font,
3271 cache,
3272 )
3273 } else {
3274 measure_text_with_font(
3275 before_newline,
3276 &segment_style,
3277 segment_font_size,
3278 segment_font,
3279 )
3280 };
3281 current_line_width += metrics.width;
3282 }
3283 max_width = max_width.max(current_line_width);
3284 current_line_width = 0.0;
3285 line_count += 1;
3286 remaining = &remaining[newline_offset + 1..];
3287 if remaining.is_empty() {
3288 break;
3289 }
3290 } else {
3291 if !remaining.is_empty() {
3292 let metrics = if let Some(cache) = cache.as_deref_mut() {
3293 measure_text_with_font_cached(
3294 remaining,
3295 &segment_style,
3296 segment_font_size,
3297 segment_font,
3298 cache,
3299 )
3300 } else {
3301 measure_text_with_font(
3302 remaining,
3303 &segment_style,
3304 segment_font_size,
3305 segment_font,
3306 )
3307 };
3308 current_line_width += metrics.width;
3309 }
3310 break;
3311 }
3312 }
3313 }
3314
3315 max_width = max_width.max(current_line_width);
3316
3317 let line_heights = annotated_line_heights_with_resolver(text, style, font_size, fonts);
3318 let total_height = line_heights.iter().sum();
3319 let max_line_height = line_heights.into_iter().fold(base_line_height, f32::max);
3320
3321 TextMetrics {
3322 width: max_width,
3323 height: total_height,
3324 line_height: max_line_height,
3325 line_count,
3326 }
3327}
3328
3329fn annotated_line_heights_with_resolver(
3330 text: &AnnotatedString,
3331 style: &TextStyle,
3332 font_size: f32,
3333 fonts: &SoftwareTextFontSet,
3334) -> Vec<f32> {
3335 let Some(base_font) = fonts.resolve(style) else {
3336 return fallback_line_heights(text.text.as_str(), style, font_size);
3337 };
3338 let base_line_height = line_height_for_style(style, font_size, &base_font.font);
3339 let mut line_heights = vec![base_line_height];
3340 let mut boundaries = text.span_boundaries();
3341 for (offset, ch) in text.text.char_indices() {
3342 if ch == '\n' {
3343 boundaries.push(offset);
3344 boundaries.push(offset + ch.len_utf8());
3345 }
3346 }
3347 boundaries.sort_unstable();
3348 boundaries.dedup();
3349 boundaries.retain(|offset| *offset <= text.text.len() && text.text.is_char_boundary(*offset));
3350
3351 let mut line_index = 0usize;
3352 for range in boundaries.windows(2) {
3353 let start = range[0];
3354 let end = range[1];
3355 if start == end {
3356 continue;
3357 }
3358 let segment = &text.text[start..end];
3359 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
3360 let segment_font_size = resolve_font_size(&segment_style);
3361 let segment_line_height = if let Some(segment_font) = fonts.resolve(&segment_style) {
3362 line_height_for_style(&segment_style, segment_font_size, &segment_font.font)
3363 } else {
3364 fallback_line_height(&segment_style, segment_font_size)
3365 };
3366 for ch in segment.chars() {
3367 line_heights[line_index] = line_heights[line_index].max(segment_line_height);
3368 if ch == '\n' {
3369 line_index += 1;
3370 if line_heights.len() <= line_index {
3371 line_heights.push(base_line_height);
3372 }
3373 }
3374 }
3375 }
3376
3377 line_heights
3378}
3379
3380fn max_line_height_for_annotated_text_with_resolver(
3381 text: &AnnotatedString,
3382 style: &TextStyle,
3383 font_size: f32,
3384 fonts: &SoftwareTextFontSet,
3385) -> f32 {
3386 let base_line_height = fonts
3387 .resolve(style)
3388 .map(|font| line_height_for_style(style, font_size, &font.font))
3389 .unwrap_or_else(|| fallback_line_height(style, font_size));
3390 if text.span_styles.is_empty() {
3391 return base_line_height;
3392 }
3393
3394 let mut max_line_height = base_line_height;
3395 for range in text.span_boundaries().windows(2) {
3396 let start = range[0];
3397 let end = range[1];
3398 if start == end {
3399 continue;
3400 }
3401 let segment_style = effective_style_for_range(&text.span_styles, style, start, end);
3402 let segment_font_size = resolve_font_size(&segment_style);
3403 let segment_line_height = fonts
3404 .resolve(&segment_style)
3405 .map(|font| line_height_for_style(&segment_style, segment_font_size, &font.font))
3406 .unwrap_or_else(|| fallback_line_height(&segment_style, segment_font_size));
3407 max_line_height = max_line_height.max(segment_line_height);
3408 }
3409 max_line_height
3410}
3411
3412fn effective_style_for_range(
3413 span_styles: &[RangeStyle<SpanStyle>],
3414 style: &TextStyle,
3415 start: usize,
3416 end: usize,
3417) -> TextStyle {
3418 let mut effective = style.clone();
3419 for span in span_styles {
3420 if span.range.start < end && span.range.end > start {
3421 effective.span_style = effective.span_style.merge(&span.item);
3422 }
3423 }
3424 effective
3425}
3426
3427fn line_height_for_style(style: &TextStyle, font_size: f32, font: &impl Font) -> f32 {
3428 let _ = font;
3429 resolve_line_height(style, font_size * 1.4)
3430}
3431
3432fn clamp_to_char_boundary(text: &str, mut offset: usize) -> usize {
3433 offset = offset.min(text.len());
3434 while offset > 0 && !text.is_char_boundary(offset) {
3435 offset -= 1;
3436 }
3437 offset
3438}
3439
3440fn align_glyph_for_text_motion(glyph: Glyph, static_text_motion: bool) -> Glyph {
3441 align_glyph_to_pixel_grid(glyph, static_text_motion)
3442}
3443
3444fn static_glyph_pixel_origin(glyph: &Glyph) -> (i32, i32) {
3445 (
3446 glyph.position.x.round() as i32,
3447 glyph.position.y.round() as i32,
3448 )
3449}
3450
3451fn glyph_mask_cache_key(
3452 font_hash: u64,
3453 glyph: &Glyph,
3454 raster_style: GlyphRasterStyle,
3455 weight_synthesis: TextWeightSynthesis,
3456 style_synthesis: TextStyleSynthesis,
3457) -> GlyphMaskCacheKey {
3458 GlyphMaskCacheKey {
3459 font_hash,
3460 glyph_id: u32::from(glyph.id.0),
3461 scale_x_bits: glyph.scale.x.to_bits(),
3462 scale_y_bits: glyph.scale.y.to_bits(),
3463 raster_style: GlyphRasterStyleKey::from_style(raster_style),
3464 embolden_px_bits: weight_synthesis.embolden_px.to_bits(),
3465 slant_bits: style_synthesis.slant.to_bits(),
3466 }
3467}
3468
3469fn glyph_atlas_key_from_mask_key(key: GlyphMaskCacheKey) -> Option<SoftwareGlyphAtlasKey> {
3470 if !matches!(key.raster_style, GlyphRasterStyleKey::Fill) {
3471 return None;
3472 }
3473 Some(SoftwareGlyphAtlasKey {
3474 font_hash: key.font_hash,
3475 glyph_id: key.glyph_id,
3476 scale_x_bits: key.scale_x_bits,
3477 scale_y_bits: key.scale_y_bits,
3478 embolden_px_bits: key.embolden_px_bits,
3479 slant_bits: key.slant_bits,
3480 })
3481}
3482
3483fn build_complete_glyph_mask(
3484 font: &impl Font,
3485 glyph: &Glyph,
3486 raster_style: GlyphRasterStyle,
3487 weight_synthesis: TextWeightSynthesis,
3488 style_synthesis: TextStyleSynthesis,
3489) -> Option<GlyphMask> {
3490 let (outlined, bounds) = outline_glyph_with_bounds(font, glyph)?;
3491 let mask = build_glyph_mask(font, glyph, &outlined, bounds, raster_style)?;
3492 let mask = synthesize_glyph_weight(mask, weight_synthesis);
3493 Some(synthesize_glyph_style(mask, style_synthesis))
3494}
3495
3496fn cached_static_glyph_mask_with_key(
3497 cache: &mut SoftwareGlyphRasterCache,
3498 font_hash: u64,
3499 font: &impl Font,
3500 glyph: &Glyph,
3501 raster_style: GlyphRasterStyle,
3502 weight_synthesis: TextWeightSynthesis,
3503 style_synthesis: TextStyleSynthesis,
3504) -> Option<(GlyphMaskCacheKey, GlyphMask)> {
3505 let key = glyph_mask_cache_key(
3506 font_hash,
3507 glyph,
3508 raster_style,
3509 weight_synthesis,
3510 style_synthesis,
3511 );
3512 if let Some(mask) = cache.get(&key, glyph) {
3513 return Some((key, mask));
3514 }
3515 let mask =
3516 build_complete_glyph_mask(font, glyph, raster_style, weight_synthesis, style_synthesis)?;
3517 Some((key, cache.put(key, glyph, mask)))
3518}
3519
3520fn cached_static_glyph_mask(
3521 cache: &mut SoftwareGlyphRasterCache,
3522 font_hash: u64,
3523 font: &impl Font,
3524 glyph: &Glyph,
3525 raster_style: GlyphRasterStyle,
3526 weight_synthesis: TextWeightSynthesis,
3527 style_synthesis: TextStyleSynthesis,
3528) -> Option<GlyphMask> {
3529 cached_static_glyph_mask_with_key(
3530 cache,
3531 font_hash,
3532 font,
3533 glyph,
3534 raster_style,
3535 weight_synthesis,
3536 style_synthesis,
3537 )
3538 .map(|(_, mask)| mask)
3539}
3540
3541#[allow(clippy::too_many_arguments)]
3542fn visit_text_glyph_masks(
3543 text: &str,
3544 font: &impl Font,
3545 font_hash: u64,
3546 font_px_size: f32,
3547 line_height: f32,
3548 first_baseline_y: f32,
3549 origin_x: f32,
3550 origin_y: f32,
3551 letter_spacing: f32,
3552 static_text_motion: bool,
3553 raster_style: GlyphRasterStyle,
3554 weight_synthesis: TextWeightSynthesis,
3555 style_synthesis: TextStyleSynthesis,
3556 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
3557 mut visit: impl FnMut(&GlyphMask),
3558) -> f32 {
3559 let scale = PxScale::from(font_px_size);
3560 let scaled_font = font.as_scaled(scale);
3561 let mut max_advance = 0.0f32;
3562 for (line_idx, line) in text.split('\n').enumerate() {
3563 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
3564 let mut caret_x = origin_x;
3565 let mut previous = None;
3566 for ch in line.chars() {
3567 let glyph_id = scaled_font.glyph_id(ch);
3568 if let Some(previous_id) = previous {
3569 caret_x += scaled_font.kern(previous_id, glyph_id) + letter_spacing;
3570 }
3571 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
3572 caret_x += scaled_font.h_advance(glyph_id);
3573 previous = Some(glyph_id);
3574 let glyph = align_glyph_for_text_motion(glyph, static_text_motion);
3575 let Some(mask) = (if static_text_motion {
3576 glyph_cache.as_deref_mut().and_then(|cache| {
3577 cached_static_glyph_mask(
3578 cache,
3579 font_hash,
3580 font,
3581 &glyph,
3582 raster_style,
3583 weight_synthesis,
3584 style_synthesis,
3585 )
3586 })
3587 } else {
3588 None
3589 })
3590 .or_else(|| {
3591 build_complete_glyph_mask(
3592 font,
3593 &glyph,
3594 raster_style,
3595 weight_synthesis,
3596 style_synthesis,
3597 )
3598 }) else {
3599 continue;
3600 };
3601 visit(&mask);
3602 }
3603 max_advance = max_advance.max((caret_x - origin_x).max(0.0));
3604 }
3605 max_advance
3606}
3607
3608#[allow(clippy::too_many_arguments)]
3609fn visit_text_glyph_masks_with_key(
3610 text: &str,
3611 font: &impl Font,
3612 font_hash: u64,
3613 font_px_size: f32,
3614 line_height: f32,
3615 first_baseline_y: f32,
3616 origin_x: f32,
3617 origin_y: f32,
3618 letter_spacing: f32,
3619 static_text_motion: bool,
3620 raster_style: GlyphRasterStyle,
3621 weight_synthesis: TextWeightSynthesis,
3622 style_synthesis: TextStyleSynthesis,
3623 mut glyph_cache: Option<&mut SoftwareGlyphRasterCache>,
3624 mut visit: impl FnMut(SoftwareGlyphAtlasKey, &GlyphMask),
3625) -> f32 {
3626 if !static_text_motion {
3627 return 0.0;
3628 }
3629
3630 let scale = PxScale::from(font_px_size);
3631 let scaled_font = font.as_scaled(scale);
3632 let mut max_advance = 0.0f32;
3633 for (line_idx, line) in text.split('\n').enumerate() {
3634 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
3635 let mut caret_x = origin_x;
3636 let mut previous = None;
3637 for ch in line.chars() {
3638 let glyph_id = scaled_font.glyph_id(ch);
3639 if let Some(previous_id) = previous {
3640 caret_x += scaled_font.kern(previous_id, glyph_id) + letter_spacing;
3641 }
3642 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
3643 caret_x += scaled_font.h_advance(glyph_id);
3644 previous = Some(glyph_id);
3645 let glyph = align_glyph_for_text_motion(glyph, true);
3646 let Some((cache_key, mask)) = glyph_cache.as_deref_mut().and_then(|cache| {
3647 cached_static_glyph_mask_with_key(
3648 cache,
3649 font_hash,
3650 font,
3651 &glyph,
3652 raster_style,
3653 weight_synthesis,
3654 style_synthesis,
3655 )
3656 }) else {
3657 continue;
3658 };
3659 let Some(atlas_key) = glyph_atlas_key_from_mask_key(cache_key) else {
3660 continue;
3661 };
3662 visit(atlas_key, &mask);
3663 }
3664 max_advance = max_advance.max((caret_x - origin_x).max(0.0));
3665 }
3666 max_advance
3667}
3668
3669#[allow(clippy::too_many_arguments)]
3670fn visit_cached_text_glyph_atlas_placements(
3671 text: &str,
3672 font: &impl Font,
3673 font_hash: u64,
3674 font_px_size: f32,
3675 line_height: f32,
3676 first_baseline_y: f32,
3677 origin_x: f32,
3678 origin_y: f32,
3679 letter_spacing: f32,
3680 raster_style: GlyphRasterStyle,
3681 weight_synthesis: TextWeightSynthesis,
3682 style_synthesis: TextStyleSynthesis,
3683 glyph_cache: &mut SoftwareGlyphRasterCache,
3684 mut visit: impl FnMut(SoftwareGlyphAtlasPlacement),
3685) -> f32 {
3686 let scale = PxScale::from(font_px_size);
3687 let scaled_font = font.as_scaled(scale);
3688 let mut max_advance = 0.0f32;
3689 for (line_idx, line) in text.split('\n').enumerate() {
3690 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
3691 let mut caret_x = origin_x;
3692 let mut previous = None;
3693 for ch in line.chars() {
3694 let glyph_id = scaled_font.glyph_id(ch);
3695 if let Some(previous_id) = previous {
3696 caret_x += scaled_font.kern(previous_id, glyph_id) + letter_spacing;
3697 }
3698 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
3699 caret_x += scaled_font.h_advance(glyph_id);
3700 previous = Some(glyph_id);
3701 let glyph = align_glyph_for_text_motion(glyph, true);
3702 let cache_key = glyph_mask_cache_key(
3703 font_hash,
3704 &glyph,
3705 raster_style,
3706 weight_synthesis,
3707 style_synthesis,
3708 );
3709 let Some((key, x, y, width, height)) =
3710 glyph_cache.get_atlas_placement(&cache_key, &glyph)
3711 else {
3712 if font.outline(glyph.id).is_none() {
3713 continue;
3714 }
3715 return f32::NAN;
3716 };
3717 visit(SoftwareGlyphAtlasPlacement {
3718 key,
3719 x,
3720 y,
3721 width,
3722 height,
3723 color: Color::WHITE,
3724 });
3725 }
3726 max_advance = max_advance.max((caret_x - origin_x).max(0.0));
3727 }
3728 max_advance
3729}
3730
3731#[allow(clippy::too_many_arguments)]
3732fn visit_text_glyph_atlas_run(
3733 text: &str,
3734 font: &impl Font,
3735 font_hash: u64,
3736 font_px_size: f32,
3737 line_height: f32,
3738 first_baseline_y: f32,
3739 origin_x: f32,
3740 origin_y: f32,
3741 letter_spacing: f32,
3742 raster_style: GlyphRasterStyle,
3743 weight_synthesis: TextWeightSynthesis,
3744 style_synthesis: TextStyleSynthesis,
3745 glyph_cache: &mut SoftwareGlyphRasterCache,
3746 mut visit: impl FnMut(SoftwareGlyphAtlasRunGlyph),
3747) -> f32 {
3748 let scale = PxScale::from(font_px_size);
3749 let scaled_font = font.as_scaled(scale);
3750 let mut max_advance = 0.0f32;
3751 let mut run_metrics_cache: Vec<(GlyphMaskCacheKey, CachedAtlasGlyphMetrics)> = Vec::new();
3752 for (line_idx, line) in text.split('\n').enumerate() {
3753 let baseline_y = first_baseline_y + line_idx as f32 * line_height + origin_y;
3754 let mut caret_x = origin_x;
3755 let mut previous = None;
3756 for ch in line.chars() {
3757 let glyph_id = scaled_font.glyph_id(ch);
3758 if let Some(previous_id) = previous {
3759 caret_x += scaled_font.kern(previous_id, glyph_id) + letter_spacing;
3760 }
3761 let glyph = glyph_id.with_scale_and_position(scale, point(caret_x, baseline_y));
3762 caret_x += scaled_font.h_advance(glyph_id);
3763 previous = Some(glyph_id);
3764 let glyph = align_glyph_for_text_motion(glyph, true);
3765 let cache_key = glyph_mask_cache_key(
3766 font_hash,
3767 &glyph,
3768 raster_style,
3769 weight_synthesis,
3770 style_synthesis,
3771 );
3772 if let Some((_, metrics)) = run_metrics_cache
3773 .iter()
3774 .find(|(cached_key, _)| *cached_key == cache_key)
3775 {
3776 visit(SoftwareGlyphAtlasRunGlyph::Cached(
3777 metrics.placement(&glyph, Color::WHITE),
3778 ));
3779 continue;
3780 }
3781 if let Some(metrics) = glyph_cache.get_atlas_metrics(&cache_key) {
3782 if run_metrics_cache.len() < RUN_GLYPH_METRICS_CACHE_LIMIT {
3783 run_metrics_cache.push((cache_key, metrics));
3784 }
3785 visit(SoftwareGlyphAtlasRunGlyph::Cached(
3786 metrics.placement(&glyph, Color::WHITE),
3787 ));
3788 continue;
3789 }
3790
3791 if font.outline(glyph.id).is_none() {
3792 continue;
3793 }
3794 let Some(mask) = build_complete_glyph_mask(
3795 font,
3796 &glyph,
3797 raster_style,
3798 weight_synthesis,
3799 style_synthesis,
3800 ) else {
3801 continue;
3802 };
3803 let mask = glyph_cache.put(cache_key, &glyph, mask);
3804 let Some(key) = glyph_atlas_key_from_mask_key(cache_key) else {
3805 continue;
3806 };
3807 let (glyph_x, glyph_y) = static_glyph_pixel_origin(&glyph);
3808 if run_metrics_cache.len() < RUN_GLYPH_METRICS_CACHE_LIMIT {
3809 run_metrics_cache.push((
3810 cache_key,
3811 CachedAtlasGlyphMetrics {
3812 key,
3813 width: mask.width,
3814 height: mask.height,
3815 origin_offset_x: mask.origin_x - glyph_x,
3816 origin_offset_y: mask.origin_y - glyph_y,
3817 },
3818 ));
3819 }
3820 visit(SoftwareGlyphAtlasRunGlyph::New(SoftwareGlyphAtlasGlyph {
3821 key,
3822 mask: SoftwareGlyphAtlasMask {
3823 alpha: Arc::clone(&mask.alpha),
3824 width: mask.width,
3825 height: mask.height,
3826 },
3827 x: mask.origin_x,
3828 y: mask.origin_y,
3829 color: Color::WHITE,
3830 }));
3831 }
3832 max_advance = max_advance.max((caret_x - origin_x).max(0.0));
3833 }
3834 max_advance
3835}
3836
3837fn blend_src_over(dst: &mut [f32; 4], src: [f32; 4]) {
3838 let src_alpha = src[3].clamp(0.0, 1.0);
3839 if src_alpha <= 0.0 {
3840 return;
3841 }
3842
3843 let dst_alpha = dst[3].clamp(0.0, 1.0);
3844 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
3845
3846 if out_alpha <= f32::EPSILON {
3847 *dst = [0.0, 0.0, 0.0, 0.0];
3848 return;
3849 }
3850
3851 for channel in 0..3 {
3852 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
3853 let dst_premult = dst[channel].clamp(0.0, 1.0) * dst_alpha;
3854 dst[channel] =
3855 ((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0);
3856 }
3857 dst[3] = out_alpha;
3858}
3859
3860fn draw_mask_glyph(
3861 canvas: &mut [[f32; 4]],
3862 width: u32,
3863 height: u32,
3864 mask: &GlyphMask,
3865 brush: &Brush,
3866 brush_alpha_multiplier: f32,
3867 brush_rect: Rect,
3868) {
3869 for y in 0..mask.height {
3870 let py = mask.origin_y + y as i32;
3871 if py < 0 || py >= height as i32 {
3872 continue;
3873 }
3874
3875 for x in 0..mask.width {
3876 let px = mask.origin_x + x as i32;
3877 if px < 0 || px >= width as i32 {
3878 continue;
3879 }
3880
3881 let coverage = mask.alpha[y * mask.width + x];
3882 if coverage <= 0.0 {
3883 continue;
3884 }
3885
3886 let sample = sample_brush_rgba(
3887 brush,
3888 brush_rect,
3889 brush_rect.x + px as f32 + 0.5,
3890 brush_rect.y + py as f32 + 0.5,
3891 );
3892 let alpha = coverage * sample[3] * brush_alpha_multiplier;
3893 if alpha <= 0.0 {
3894 continue;
3895 }
3896 let idx = (py as u32 * width + px as u32) as usize;
3897 blend_src_over(
3898 &mut canvas[idx],
3899 [sample[0], sample[1], sample[2], alpha.clamp(0.0, 1.0)],
3900 );
3901 }
3902 }
3903}
3904
3905fn blend_src_over_u8(dst: &mut [u8], src: [f32; 4]) {
3906 let src_alpha = src[3].clamp(0.0, 1.0);
3907 if src_alpha <= 0.0 {
3908 return;
3909 }
3910
3911 let dst_alpha = dst[3] as f32 / 255.0;
3912 if dst_alpha <= 0.0 {
3913 dst[0] = (src[0].clamp(0.0, 1.0) * 255.0).round() as u8;
3914 dst[1] = (src[1].clamp(0.0, 1.0) * 255.0).round() as u8;
3915 dst[2] = (src[2].clamp(0.0, 1.0) * 255.0).round() as u8;
3916 dst[3] = (src_alpha * 255.0).round() as u8;
3917 return;
3918 }
3919
3920 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
3921 if out_alpha <= f32::EPSILON {
3922 dst.fill(0);
3923 return;
3924 }
3925
3926 for channel in 0..3 {
3927 let src_premult = src[channel].clamp(0.0, 1.0) * src_alpha;
3928 let dst_premult = (dst[channel] as f32 / 255.0) * dst_alpha;
3929 dst[channel] =
3930 ((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha * 255.0).round() as u8;
3931 }
3932 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
3933}
3934
3935fn draw_mask_glyph_solid_u8(
3936 canvas: &mut [u8],
3937 width: u32,
3938 height: u32,
3939 mask: &GlyphMask,
3940 color: [f32; 4],
3941 alpha_multiplier: f32,
3942) {
3943 let red = (color[0].clamp(0.0, 1.0) * 255.0).round() as u8;
3944 let green = (color[1].clamp(0.0, 1.0) * 255.0).round() as u8;
3945 let blue = (color[2].clamp(0.0, 1.0) * 255.0).round() as u8;
3946 let alpha_scale = color[3].clamp(0.0, 1.0) * alpha_multiplier.clamp(0.0, 1.0);
3947 if alpha_scale <= 0.0 {
3948 return;
3949 }
3950
3951 for y in 0..mask.height {
3952 let py = mask.origin_y + y as i32;
3953 if py < 0 || py >= height as i32 {
3954 continue;
3955 }
3956
3957 for x in 0..mask.width {
3958 let px = mask.origin_x + x as i32;
3959 if px < 0 || px >= width as i32 {
3960 continue;
3961 }
3962
3963 let coverage = mask.alpha[y * mask.width + x];
3964 if coverage <= 0.0 {
3965 continue;
3966 }
3967
3968 let alpha = (coverage * alpha_scale).clamp(0.0, 1.0);
3969 let alpha_u8 = (alpha * 255.0).round() as u8;
3970 if alpha_u8 == 0 {
3971 continue;
3972 }
3973 let idx = ((py as u32 * width + px as u32) * 4) as usize;
3974 let dst = &mut canvas[idx..idx + 4];
3975 if dst[3] == 0 {
3976 dst[0] = red;
3977 dst[1] = green;
3978 dst[2] = blue;
3979 dst[3] = alpha_u8;
3980 } else {
3981 blend_src_over_u8(dst, [color[0], color[1], color[2], alpha]);
3982 }
3983 }
3984 }
3985}
3986
3987fn draw_shadow_mask(
3988 canvas: &mut [[f32; 4]],
3989 width: u32,
3990 height: u32,
3991 mask: &GlyphMask,
3992 shadow: Shadow,
3993 text_scale: f32,
3994 static_text_motion: bool,
3995) {
3996 if mask.width == 0 || mask.height == 0 {
3997 return;
3998 }
3999
4000 let shadow_dx = shadow.offset.x * text_scale;
4001 let shadow_dy = shadow.offset.y * text_scale;
4002 let blur_radius = (shadow.blur_radius * text_scale).max(0.0);
4003 let sigma = shadow_blur_sigma(blur_radius);
4004 let blur_margin = if sigma > 0.0 {
4005 (sigma * 3.0).ceil() as i32
4006 } else {
4007 0
4008 };
4009
4010 let padded_width = mask.width + (blur_margin as usize) * 2;
4011 let padded_height = mask.height + (blur_margin as usize) * 2;
4012 let mut padded_mask = vec![0.0f32; padded_width * padded_height];
4013
4014 for y in 0..mask.height {
4015 let src_offset = y * mask.width;
4016 let dst_offset = (y + blur_margin as usize) * padded_width + blur_margin as usize;
4017 padded_mask[dst_offset..dst_offset + mask.width]
4018 .copy_from_slice(&mask.alpha[src_offset..src_offset + mask.width]);
4019 }
4020
4021 let blurred = if sigma > 0.0 {
4022 gaussian_blur_alpha(&padded_mask, padded_width, padded_height, sigma)
4023 } else {
4024 padded_mask
4025 };
4026
4027 let shadow_rgba = color_to_rgba(shadow.color);
4028 let shadow_origin_x = mask.origin_x - blur_margin;
4029 let shadow_origin_y = mask.origin_y - blur_margin;
4030
4031 for y in 0..padded_height {
4032 for x in 0..padded_width {
4033 let alpha = blurred[y * padded_width + x] * shadow_rgba[3];
4034 if alpha <= 0.0 {
4035 continue;
4036 }
4037
4038 let target_x = shadow_origin_x as f32 + x as f32 + shadow_dx;
4039 let target_y = shadow_origin_y as f32 + y as f32 + shadow_dy;
4040 if static_text_motion {
4041 blend_shadow_pixel(
4042 canvas,
4043 width,
4044 height,
4045 target_x.round() as i32,
4046 target_y.round() as i32,
4047 shadow_rgba,
4048 alpha.clamp(0.0, 1.0),
4049 );
4050 } else {
4051 blend_shadow_pixel_subpixel(
4052 canvas,
4053 width,
4054 height,
4055 target_x,
4056 target_y,
4057 shadow_rgba,
4058 alpha.clamp(0.0, 1.0),
4059 );
4060 }
4061 }
4062 }
4063}
4064
4065fn blend_shadow_pixel(
4066 canvas: &mut [[f32; 4]],
4067 width: u32,
4068 height: u32,
4069 px: i32,
4070 py: i32,
4071 color: [f32; 4],
4072 alpha: f32,
4073) {
4074 if px < 0 || py < 0 || px >= width as i32 || py >= height as i32 || alpha <= 0.0 {
4075 return;
4076 }
4077 let idx = (py as u32 * width + px as u32) as usize;
4078 blend_src_over(
4079 &mut canvas[idx],
4080 [color[0], color[1], color[2], alpha.clamp(0.0, 1.0)],
4081 );
4082}
4083
4084fn blend_shadow_pixel_subpixel(
4085 canvas: &mut [[f32; 4]],
4086 width: u32,
4087 height: u32,
4088 x: f32,
4089 y: f32,
4090 color: [f32; 4],
4091 alpha: f32,
4092) {
4093 if alpha <= 0.0 {
4094 return;
4095 }
4096
4097 let base_x = x.floor();
4098 let base_y = y.floor();
4099 let frac_x = x - base_x;
4100 let frac_y = y - base_y;
4101 let base_x_i32 = base_x as i32;
4102 let base_y_i32 = base_y as i32;
4103 let weights = [
4104 ((1.0 - frac_x) * (1.0 - frac_y), 0i32, 0i32),
4105 (frac_x * (1.0 - frac_y), 1, 0),
4106 ((1.0 - frac_x) * frac_y, 0, 1),
4107 (frac_x * frac_y, 1, 1),
4108 ];
4109
4110 for (weight, dx, dy) in weights {
4111 if weight <= 0.0 {
4112 continue;
4113 }
4114 blend_shadow_pixel(
4115 canvas,
4116 width,
4117 height,
4118 base_x_i32 + dx,
4119 base_y_i32 + dy,
4120 color,
4121 alpha * weight,
4122 );
4123 }
4124}
4125
4126fn shadow_blur_sigma(blur_radius: f32) -> f32 {
4127 if blur_radius <= 0.0 {
4128 0.0
4129 } else {
4130 (blur_radius * SHADOW_SIGMA_SCALE + SHADOW_SIGMA_BIAS).max(0.5)
4131 }
4132}
4133
4134fn gaussian_blur_alpha(src: &[f32], width: usize, height: usize, sigma: f32) -> Vec<f32> {
4135 let kernel = gaussian_kernel_1d(sigma);
4136 if kernel.len() == 1 {
4137 return src.to_vec();
4138 }
4139 let half = (kernel.len() / 2) as i32;
4140
4141 let mut horizontal = vec![0.0f32; src.len()];
4142 for y in 0..height {
4143 for x in 0..width {
4144 let mut sum = 0.0f32;
4145 for (index, weight) in kernel.iter().enumerate() {
4146 let offset = index as i32 - half;
4147 let sample_x = (x as i32 + offset).clamp(0, width as i32 - 1) as usize;
4148 sum += src[y * width + sample_x] * *weight;
4149 }
4150 horizontal[y * width + x] = sum;
4151 }
4152 }
4153
4154 let mut output = vec![0.0f32; src.len()];
4155 for y in 0..height {
4156 for x in 0..width {
4157 let mut sum = 0.0f32;
4158 for (index, weight) in kernel.iter().enumerate() {
4159 let offset = index as i32 - half;
4160 let sample_y = (y as i32 + offset).clamp(0, height as i32 - 1) as usize;
4161 sum += horizontal[sample_y * width + x] * *weight;
4162 }
4163 output[y * width + x] = sum;
4164 }
4165 }
4166
4167 output
4168}
4169
4170fn gaussian_kernel_1d(sigma: f32) -> Vec<f32> {
4171 let half = ((sigma * 3.0).ceil() as i32).clamp(1, MAX_GAUSSIAN_KERNEL_HALF);
4172 if half <= 0 {
4173 return vec![1.0];
4174 }
4175
4176 let mut kernel = Vec::with_capacity((half * 2 + 1) as usize);
4177 let mut sum = 0.0f32;
4178 for offset in -half..=half {
4179 let distance = offset as f32;
4180 let weight = (-0.5 * (distance / sigma).powi(2)).exp();
4181 kernel.push(weight);
4182 sum += weight;
4183 }
4184
4185 if sum > f32::EPSILON {
4186 for weight in &mut kernel {
4187 *weight /= sum;
4188 }
4189 }
4190
4191 kernel
4192}
4193
4194fn outline_glyph_with_bounds(
4195 font: &impl Font,
4196 glyph: &Glyph,
4197) -> Option<(OutlinedGlyph, GlyphPixelBounds)> {
4198 let outlined = font.outline_glyph(glyph.clone())?;
4199 let bounds = pixel_bounds_from_outlined(&outlined);
4200 Some((outlined, bounds))
4201}
4202
4203fn build_glyph_mask(
4204 font: &impl Font,
4205 glyph: &Glyph,
4206 outlined: &OutlinedGlyph,
4207 bounds: GlyphPixelBounds,
4208 style: GlyphRasterStyle,
4209) -> Option<GlyphMask> {
4210 match style {
4211 GlyphRasterStyle::Fill => build_fill_mask(outlined, bounds),
4212 GlyphRasterStyle::Stroke { width_px } => {
4213 build_stroke_mask(font, glyph, outlined, bounds, width_px)
4214 }
4215 }
4216}
4217
4218fn build_fill_mask(outlined: &OutlinedGlyph, bounds: GlyphPixelBounds) -> Option<GlyphMask> {
4219 let mask_width = bounds.width();
4220 let mask_height = bounds.height();
4221 if mask_width == 0 || mask_height == 0 {
4222 return None;
4223 }
4224
4225 let mut alpha = vec![0.0f32; mask_width * mask_height];
4226 outlined.draw(|gx, gy, value| {
4227 let idx = gy as usize * mask_width + gx as usize;
4228 alpha[idx] = value;
4229 });
4230
4231 Some(GlyphMask {
4232 alpha: Arc::from(alpha),
4233 width: mask_width,
4234 height: mask_height,
4235 origin_x: bounds.min_x,
4236 origin_y: bounds.min_y,
4237 })
4238}
4239
4240fn build_stroke_mask(
4241 font: &impl Font,
4242 glyph: &Glyph,
4243 outlined: &OutlinedGlyph,
4244 bounds: GlyphPixelBounds,
4245 stroke_width_px: f32,
4246) -> Option<GlyphMask> {
4247 if !stroke_width_px.is_finite() || stroke_width_px <= 0.0 {
4248 return build_fill_mask(outlined, bounds);
4249 }
4250
4251 let mask_width = bounds.max_x - bounds.min_x;
4252 let mask_height = bounds.max_y - bounds.min_y;
4253 if mask_width <= 0 || mask_height <= 0 {
4254 return None;
4255 }
4256
4257 let half_width = stroke_width_px * 0.5;
4258 let miter_pad = (half_width * COMPOSE_STROKE_MITER_LIMIT).ceil();
4259 let pad = miter_pad.max(1.0) as i32 + 1;
4260 let path = build_outline_path(font, glyph, bounds, pad)?;
4261 let raster_width = mask_width + pad * 2;
4262 let raster_height = mask_height + pad * 2;
4263 if raster_width <= 0 || raster_height <= 0 {
4264 return None;
4265 }
4266
4267 let mut pixmap = Pixmap::new(raster_width as u32, raster_height as u32)?;
4268 let mut paint = Paint::default();
4269 paint.set_color_rgba8(255, 255, 255, 255);
4270 paint.anti_alias = true;
4271
4272 let stroke = Stroke {
4273 width: stroke_width_px,
4274 line_cap: LineCap::Butt,
4275 line_join: LineJoin::Miter,
4276 miter_limit: COMPOSE_STROKE_MITER_LIMIT,
4277 ..Stroke::default()
4278 };
4279
4280 pixmap.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
4281
4282 let alpha: Vec<f32> = pixmap
4283 .data()
4284 .chunks_exact(4)
4285 .map(|pixel| pixel[3] as f32 / 255.0)
4286 .collect();
4287
4288 Some(GlyphMask {
4289 alpha: Arc::from(alpha),
4290 width: raster_width as usize,
4291 height: raster_height as usize,
4292 origin_x: bounds.min_x - pad,
4293 origin_y: bounds.min_y - pad,
4294 })
4295}
4296
4297fn synthesize_glyph_weight(mask: GlyphMask, synthesis: TextWeightSynthesis) -> GlyphMask {
4298 let horizontal_shift = synthetic_weight_shift_px(synthesis.embolden_px);
4299 if horizontal_shift == 0 || mask.width == 0 || mask.height == 0 {
4300 return mask;
4301 }
4302
4303 let vertical_shift = (horizontal_shift / 2).min(1);
4304 let output_width = mask.width + horizontal_shift;
4305 let output_height = mask.height + vertical_shift * 2;
4306 let mut alpha = vec![0.0f32; output_width * output_height];
4307 for y in 0..mask.height {
4308 for x in 0..mask.width {
4309 let coverage = mask.alpha[y * mask.width + x];
4310 if coverage <= 0.0 {
4311 continue;
4312 }
4313 for dy in 0..=(vertical_shift * 2) {
4314 let output_y = y + dy;
4315 for dx in 0..=horizontal_shift {
4316 let output_x = x + dx;
4317 let output_index = output_y * output_width + output_x;
4318 if coverage > alpha[output_index] {
4319 alpha[output_index] = coverage;
4320 }
4321 }
4322 }
4323 }
4324 }
4325
4326 GlyphMask {
4327 alpha: Arc::from(alpha),
4328 width: output_width,
4329 height: output_height,
4330 origin_x: mask.origin_x,
4331 origin_y: mask.origin_y - vertical_shift as i32,
4332 }
4333}
4334
4335fn synthesize_glyph_style(mask: GlyphMask, synthesis: TextStyleSynthesis) -> GlyphMask {
4336 if synthesis.slant <= 0.0 || mask.width == 0 || mask.height == 0 {
4337 return mask;
4338 }
4339
4340 let max_shift = ((mask.height.saturating_sub(1)) as f32 * synthesis.slant).ceil() as usize;
4341 if max_shift == 0 {
4342 return mask;
4343 }
4344
4345 let output_width = mask.width + max_shift + 1;
4346 let mut alpha = vec![0.0f32; output_width * mask.height];
4347 for y in 0..mask.height {
4348 let shift = (mask.height.saturating_sub(1) - y) as f32 * synthesis.slant;
4349 let shift_floor = shift.floor() as usize;
4350 let shift_fraction = shift - shift.floor();
4351 for x in 0..mask.width {
4352 let coverage = mask.alpha[y * mask.width + x];
4353 if coverage <= 0.0 {
4354 continue;
4355 }
4356
4357 let output_x = x + shift_floor;
4358 let left_index = y * output_width + output_x;
4359 let left_coverage = coverage * (1.0 - shift_fraction);
4360 if left_coverage > alpha[left_index] {
4361 alpha[left_index] = left_coverage;
4362 }
4363
4364 if shift_fraction > 0.0 {
4365 let right_index = left_index + 1;
4366 let right_coverage = coverage * shift_fraction;
4367 if right_coverage > alpha[right_index] {
4368 alpha[right_index] = right_coverage;
4369 }
4370 }
4371 }
4372 }
4373
4374 GlyphMask {
4375 alpha: Arc::from(alpha),
4376 width: output_width,
4377 height: mask.height,
4378 origin_x: mask.origin_x,
4379 origin_y: mask.origin_y,
4380 }
4381}
4382
4383fn synthetic_weight_shift_px(embolden_px: f32) -> usize {
4384 if !embolden_px.is_finite() || embolden_px < 0.35 {
4385 return 0;
4386 }
4387 embolden_px.ceil().max(1.0) as usize
4388}
4389
4390fn build_outline_path(
4391 font: &impl Font,
4392 glyph: &Glyph,
4393 bounds: GlyphPixelBounds,
4394 pad: i32,
4395) -> Option<Path> {
4396 let outline = font.outline(glyph.id)?;
4397 let scale_factor = font.as_scaled(glyph.scale).scale_factor();
4398 let mut builder = PathBuilder::new();
4399 let mut has_segments = false;
4400 let mut current_end = None;
4401 let mut subpath_start = None;
4402
4403 for curve in outline.curves {
4404 match curve {
4405 ab_glyph::OutlineCurve::Line(p0, p1) => {
4406 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
4407 let end = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
4408 if current_end != Some(start) {
4409 if current_end.is_some() {
4410 builder.close();
4411 }
4412 builder.move_to(start.0, start.1);
4413 subpath_start = Some(start);
4414 }
4415 builder.line_to(end.0, end.1);
4416 if subpath_start == Some(end) {
4417 builder.close();
4418 current_end = None;
4419 subpath_start = None;
4420 } else {
4421 current_end = Some(end);
4422 }
4423 }
4424 ab_glyph::OutlineCurve::Quad(p0, p1, p2) => {
4425 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
4426 let control = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
4427 let end = transform_outline_point(p2, scale_factor, glyph, bounds, pad);
4428 if current_end != Some(start) {
4429 if current_end.is_some() {
4430 builder.close();
4431 }
4432 builder.move_to(start.0, start.1);
4433 subpath_start = Some(start);
4434 }
4435 builder.quad_to(control.0, control.1, end.0, end.1);
4436 if subpath_start == Some(end) {
4437 builder.close();
4438 current_end = None;
4439 subpath_start = None;
4440 } else {
4441 current_end = Some(end);
4442 }
4443 }
4444 ab_glyph::OutlineCurve::Cubic(p0, p1, p2, p3) => {
4445 let start = transform_outline_point(p0, scale_factor, glyph, bounds, pad);
4446 let control1 = transform_outline_point(p1, scale_factor, glyph, bounds, pad);
4447 let control2 = transform_outline_point(p2, scale_factor, glyph, bounds, pad);
4448 let end = transform_outline_point(p3, scale_factor, glyph, bounds, pad);
4449 if current_end != Some(start) {
4450 if current_end.is_some() {
4451 builder.close();
4452 }
4453 builder.move_to(start.0, start.1);
4454 subpath_start = Some(start);
4455 }
4456 builder.cubic_to(control1.0, control1.1, control2.0, control2.1, end.0, end.1);
4457 if subpath_start == Some(end) {
4458 builder.close();
4459 current_end = None;
4460 subpath_start = None;
4461 } else {
4462 current_end = Some(end);
4463 }
4464 }
4465 }
4466 has_segments = true;
4467 }
4468
4469 if !has_segments {
4470 return None;
4471 }
4472
4473 if current_end.is_some() {
4474 builder.close();
4475 }
4476
4477 builder.finish()
4478}
4479
4480fn transform_outline_point(
4481 point: ab_glyph::Point,
4482 scale_factor: ab_glyph::PxScaleFactor,
4483 glyph: &Glyph,
4484 bounds: GlyphPixelBounds,
4485 pad: i32,
4486) -> (f32, f32) {
4487 (
4488 point.x * scale_factor.horizontal + glyph.position.x - bounds.min_x as f32 + pad as f32,
4489 point.y * -scale_factor.vertical + glyph.position.y - bounds.min_y as f32 + pad as f32,
4490 )
4491}
4492
4493#[cfg(test)]
4494mod tests {
4495 use super::*;
4496 use cranpose_ui::text::{RangeStyle, SpanStyle};
4497 use cranpose_ui_graphics::Point;
4498
4499 fn count_ink_pixels(image: &ImageBitmap) -> usize {
4500 image
4501 .pixels()
4502 .chunks_exact(4)
4503 .filter(|px| px[3] > 0)
4504 .count()
4505 }
4506
4507 #[test]
4508 fn software_glyph_raster_cache_reuses_static_masks_across_positions() {
4509 let font = default_software_text_font().expect("bundled default font");
4510 let style = TextStyle::default();
4511 let rect = Rect {
4512 x: 0.0,
4513 y: 0.0,
4514 width: 160.0,
4515 height: 32.0,
4516 };
4517 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4518
4519 let uncached = rasterize_text_to_image(
4520 "aaaa",
4521 rect,
4522 &style,
4523 Color(1.0, 1.0, 1.0, 1.0),
4524 18.0,
4525 1.0,
4526 &font,
4527 )
4528 .expect("uncached image");
4529 let cached = rasterize_text_to_image_with_glyph_cache(
4530 "aaaa",
4531 rect,
4532 &style,
4533 Color(1.0, 1.0, 1.0, 1.0),
4534 18.0,
4535 1.0,
4536 &font,
4537 &mut cache,
4538 )
4539 .expect("cached image");
4540
4541 assert_eq!(cached.pixels(), uncached.pixels());
4542 let stats = cache.stats();
4543 assert_eq!(stats.entries, 1);
4544 assert_eq!(stats.misses, 1);
4545 assert_eq!(stats.hits, 3);
4546
4547 let shifted_rect = Rect {
4548 x: 24.0,
4549 y: 17.0,
4550 ..rect
4551 };
4552 let _ = rasterize_text_to_image_with_glyph_cache(
4553 "aaaa",
4554 shifted_rect,
4555 &style,
4556 Color(1.0, 1.0, 1.0, 1.0),
4557 18.0,
4558 1.0,
4559 &font,
4560 &mut cache,
4561 )
4562 .expect("cached shifted image");
4563
4564 let shifted_stats = cache.stats();
4565 assert_eq!(shifted_stats.entries, 1);
4566 assert_eq!(shifted_stats.misses, 1);
4567 assert_eq!(shifted_stats.hits, 7);
4568 }
4569
4570 #[test]
4571 fn annotated_solid_text_direct_raster_matches_plain_text_pixels() {
4572 let font = default_software_text_font().expect("bundled default font");
4573 let font_set = SoftwareTextFontSet::from_font(font.clone());
4574 let style = TextStyle::default();
4575 let rect = Rect {
4576 x: 0.0,
4577 y: 0.0,
4578 width: 240.0,
4579 height: 40.0,
4580 };
4581 let color = Color(1.0, 1.0, 1.0, 1.0);
4582 let annotated = AnnotatedString {
4583 text: "plain link".to_string(),
4584 span_styles: vec![RangeStyle {
4585 item: SpanStyle {
4586 color: Some(color),
4587 ..Default::default()
4588 },
4589 range: 0..10,
4590 }],
4591 ..Default::default()
4592 };
4593 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4594
4595 let plain = rasterize_text_to_image(
4596 annotated.text.as_str(),
4597 rect,
4598 &style,
4599 color,
4600 18.0,
4601 1.0,
4602 &font,
4603 )
4604 .expect("plain text image");
4605 let direct = rasterize_annotated_text_to_image_with_glyph_cache(
4606 &annotated, rect, &style, color, 18.0, 1.0, &font_set, &mut cache,
4607 )
4608 .expect("annotated text image");
4609
4610 assert_eq!(direct.pixels(), plain.pixels());
4611 }
4612
4613 #[test]
4614 fn solid_annotated_text_collects_atlas_glyphs_with_stable_keys() {
4615 let font = default_software_text_font().expect("bundled default font");
4616 let font_set = SoftwareTextFontSet::from_font(font);
4617 let style = TextStyle::default();
4618 let rect = Rect {
4619 x: 12.0,
4620 y: 4.0,
4621 width: 260.0,
4622 height: 48.0,
4623 };
4624 let annotated = AnnotatedString {
4625 text: "markdown link".to_string(),
4626 span_styles: vec![RangeStyle {
4627 item: SpanStyle {
4628 color: Some(Color(0.4, 0.7, 1.0, 1.0)),
4629 ..Default::default()
4630 },
4631 range: 9..13,
4632 }],
4633 ..Default::default()
4634 };
4635 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4636 let mut glyphs = Vec::new();
4637
4638 collect_solid_text_atlas_glyphs(
4639 &annotated,
4640 rect,
4641 &style,
4642 Color::WHITE,
4643 18.0,
4644 1.0,
4645 &font_set,
4646 &mut cache,
4647 &mut glyphs,
4648 )
4649 .expect("solid styled text is atlas-eligible");
4650
4651 assert!(!glyphs.is_empty());
4652 assert!(glyphs.iter().all(|glyph| glyph.mask.width > 0));
4653 assert!(glyphs.iter().all(|glyph| glyph.mask.height > 0));
4654 assert!(glyphs
4655 .iter()
4656 .any(|glyph| glyph.color == Color(0.4, 0.7, 1.0, 1.0)));
4657 assert!(cache.stats().entries > 0);
4658 }
4659
4660 #[test]
4661 fn cached_atlas_placements_reuse_existing_glyph_masks_without_payloads() {
4662 let font = default_software_text_font().expect("bundled default font");
4663 let font_set = SoftwareTextFontSet::from_font(font);
4664 let style = TextStyle::default();
4665 let rect = Rect {
4666 x: 12.0,
4667 y: 4.0,
4668 width: 260.0,
4669 height: 48.0,
4670 };
4671 let annotated = AnnotatedString {
4672 text: "markdown link".to_string(),
4673 span_styles: vec![RangeStyle {
4674 item: SpanStyle {
4675 color: Some(Color(0.4, 0.7, 1.0, 1.0)),
4676 ..Default::default()
4677 },
4678 range: 9..13,
4679 }],
4680 ..Default::default()
4681 };
4682 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4683 let mut placements = Vec::new();
4684
4685 assert!(
4686 collect_cached_solid_text_atlas_placements(
4687 &annotated,
4688 rect,
4689 &style,
4690 Color::WHITE,
4691 18.0,
4692 1.0,
4693 &font_set,
4694 &mut cache,
4695 &mut placements,
4696 )
4697 .is_none(),
4698 "placement-only collection requires retained glyph masks"
4699 );
4700 assert!(placements.is_empty());
4701
4702 let mut glyphs = Vec::new();
4703 collect_solid_text_atlas_glyphs(
4704 &annotated,
4705 rect,
4706 &style,
4707 Color::WHITE,
4708 18.0,
4709 1.0,
4710 &font_set,
4711 &mut cache,
4712 &mut glyphs,
4713 )
4714 .expect("solid styled text is atlas-eligible");
4715
4716 collect_cached_solid_text_atlas_placements(
4717 &annotated,
4718 rect,
4719 &style,
4720 Color::WHITE,
4721 18.0,
4722 1.0,
4723 &font_set,
4724 &mut cache,
4725 &mut placements,
4726 )
4727 .expect("cached masks provide placement-only atlas glyphs");
4728
4729 assert_eq!(placements.len(), glyphs.len());
4730 assert!(placements
4731 .iter()
4732 .zip(glyphs.iter())
4733 .all(|(placement, glyph)| {
4734 placement.key == glyph.key
4735 && placement.x == glyph.x
4736 && placement.y == glyph.y
4737 && placement.width == glyph.mask.width
4738 && placement.height == glyph.mask.height
4739 && placement.color == glyph.color
4740 }));
4741 let recovered = cache
4742 .atlas_glyph_for_placement(&placements[0])
4743 .expect("placement should recover retained mask payload");
4744 assert_eq!(recovered.key, glyphs[0].key);
4745 assert_eq!(recovered.x, glyphs[0].x);
4746 assert_eq!(recovered.y, glyphs[0].y);
4747 assert_eq!(recovered.mask.width, glyphs[0].mask.width);
4748 assert_eq!(recovered.mask.height, glyphs[0].mask.height);
4749 assert_eq!(recovered.mask.alpha, glyphs[0].mask.alpha);
4750 assert_eq!(recovered.color, glyphs[0].color);
4751 }
4752
4753 #[test]
4754 fn atlas_glyph_collection_rejects_shadow_and_gradient_without_partial_output() {
4755 let font = default_software_text_font().expect("bundled default font");
4756 let font_set = SoftwareTextFontSet::from_font(font);
4757 let rect = Rect {
4758 x: 0.0,
4759 y: 0.0,
4760 width: 240.0,
4761 height: 40.0,
4762 };
4763 let mut cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(64);
4764 let mut glyphs = Vec::new();
4765 glyphs.push(SoftwareGlyphAtlasGlyph {
4766 key: SoftwareGlyphAtlasKey {
4767 font_hash: 1,
4768 glyph_id: 1,
4769 scale_x_bits: 1,
4770 scale_y_bits: 1,
4771 embolden_px_bits: 0,
4772 slant_bits: 0,
4773 },
4774 mask: SoftwareGlyphAtlasMask {
4775 alpha: Arc::from([1.0f32]),
4776 width: 1,
4777 height: 1,
4778 },
4779 x: 0,
4780 y: 0,
4781 color: Color::WHITE,
4782 });
4783 let initial_len = glyphs.len();
4784
4785 let shadow_style = TextStyle::from_span_style(SpanStyle {
4786 shadow: Some(Shadow {
4787 color: Color(0.0, 0.0, 0.0, 0.5),
4788 offset: Point::new(1.0, 1.0),
4789 blur_radius: 0.0,
4790 }),
4791 ..Default::default()
4792 });
4793 assert!(collect_solid_text_atlas_glyphs(
4794 &AnnotatedString::new("shadow".to_string()),
4795 rect,
4796 &shadow_style,
4797 Color::WHITE,
4798 18.0,
4799 1.0,
4800 &font_set,
4801 &mut cache,
4802 &mut glyphs,
4803 )
4804 .is_none());
4805 assert_eq!(glyphs.len(), initial_len);
4806
4807 let gradient_style = TextStyle::from_span_style(SpanStyle {
4808 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
4809 ..Default::default()
4810 });
4811 assert!(collect_solid_text_atlas_glyphs(
4812 &AnnotatedString::new("gradient".to_string()),
4813 rect,
4814 &gradient_style,
4815 Color::WHITE,
4816 18.0,
4817 1.0,
4818 &font_set,
4819 &mut cache,
4820 &mut glyphs,
4821 )
4822 .is_none());
4823 assert_eq!(glyphs.len(), initial_len);
4824 }
4825
4826 fn average_ink_rgb(
4827 image: &ImageBitmap,
4828 x_start: u32,
4829 x_end: u32,
4830 y_start: u32,
4831 y_end: u32,
4832 ) -> Option<[f32; 3]> {
4833 let width = image.width();
4834 let height = image.height();
4835 let mut sums = [0.0f32; 3];
4836 let mut count = 0usize;
4837 let pixels = image.pixels();
4838
4839 let x_end = x_end.min(width);
4840 let y_end = y_end.min(height);
4841 for y in y_start.min(height)..y_end {
4842 for x in x_start.min(width)..x_end {
4843 let idx = ((y * width + x) * 4) as usize;
4844 let alpha = pixels[idx + 3];
4845 if alpha == 0 {
4846 continue;
4847 }
4848 sums[0] += pixels[idx] as f32 / 255.0;
4849 sums[1] += pixels[idx + 1] as f32 / 255.0;
4850 sums[2] += pixels[idx + 2] as f32 / 255.0;
4851 count += 1;
4852 }
4853 }
4854
4855 if count == 0 {
4856 return None;
4857 }
4858 Some([
4859 sums[0] / count as f32,
4860 sums[1] / count as f32,
4861 sums[2] / count as f32,
4862 ])
4863 }
4864
4865 fn ink_x_range(image: &ImageBitmap) -> Option<(u32, u32)> {
4866 let width = image.width();
4867 let height = image.height();
4868 let pixels = image.pixels();
4869 let mut min_x = u32::MAX;
4870 let mut max_x = 0u32;
4871 let mut found = false;
4872 for y in 0..height {
4873 for x in 0..width {
4874 let idx = ((y * width + x) * 4) as usize;
4875 if pixels[idx + 3] > 0 {
4876 min_x = min_x.min(x);
4877 max_x = max_x.max(x + 1);
4878 found = true;
4879 }
4880 }
4881 }
4882 found.then_some((min_x, max_x))
4883 }
4884
4885 fn ink_y_range(image: &ImageBitmap) -> Option<(u32, u32)> {
4886 let width = image.width();
4887 let height = image.height();
4888 let pixels = image.pixels();
4889 let mut min_y = u32::MAX;
4890 let mut max_y = 0u32;
4891 let mut found = false;
4892 for y in 0..height {
4893 for x in 0..width {
4894 let idx = ((y * width + x) * 4) as usize;
4895 if pixels[idx + 3] > 0 {
4896 min_y = min_y.min(y);
4897 max_y = max_y.max(y + 1);
4898 found = true;
4899 }
4900 }
4901 }
4902 found.then_some((min_y, max_y))
4903 }
4904
4905 fn ink_centroid_x(image: &ImageBitmap, y_start: u32, y_end: u32) -> Option<f32> {
4906 let width = image.width();
4907 let height = image.height();
4908 let pixels = image.pixels();
4909 let mut weighted_x = 0.0f32;
4910 let mut total_alpha = 0.0f32;
4911
4912 for y in y_start.min(height)..y_end.min(height) {
4913 for x in 0..width {
4914 let idx = ((y * width + x) * 4) as usize;
4915 let alpha = pixels[idx + 3] as f32 / 255.0;
4916 if alpha <= 0.0 {
4917 continue;
4918 }
4919 weighted_x += x as f32 * alpha;
4920 total_alpha += alpha;
4921 }
4922 }
4923
4924 (total_alpha > 0.0).then_some(weighted_x / total_alpha)
4925 }
4926
4927 fn vertical_slant_delta(image: &ImageBitmap) -> f32 {
4928 let (top, bottom) = ink_y_range(image).expect("image should contain ink");
4929 let mid = top + (bottom - top).max(1) / 2;
4930 let top_x = ink_centroid_x(image, top, mid).expect("top ink centroid");
4931 let bottom_x = ink_centroid_x(image, mid, bottom).expect("bottom ink centroid");
4932 top_x - bottom_x
4933 }
4934
4935 fn top_ink_row(image: &ImageBitmap) -> Option<u32> {
4936 let width = image.width();
4937 let height = image.height();
4938 let pixels = image.pixels();
4939 for y in 0..height {
4940 for x in 0..width {
4941 let idx = ((y * width + x) * 4) as usize;
4942 if pixels[idx + 3] > 0 {
4943 return Some(y);
4944 }
4945 }
4946 }
4947 None
4948 }
4949
4950 fn reference_dilation_offsets(radius: i32) -> Vec<(i32, i32)> {
4951 let mut offsets = Vec::new();
4952 let squared_radius = radius * radius;
4953 for dy in -radius..=radius {
4954 for dx in -radius..=radius {
4955 if dx * dx + dy * dy <= squared_radius {
4956 offsets.push((dx, dy));
4957 }
4958 }
4959 }
4960 if offsets.is_empty() {
4961 offsets.push((0, 0));
4962 }
4963 offsets
4964 }
4965
4966 fn reference_dilation_stroke_mask(fill: &GlyphMask, stroke_width: f32) -> GlyphMask {
4967 let radius = (stroke_width * 0.5).ceil() as i32;
4968 let offsets = reference_dilation_offsets(radius);
4969 let out_width = fill.width as i32 + radius * 2;
4970 let out_height = fill.height as i32 + radius * 2;
4971 let fill_width_i32 = fill.width as i32;
4972 let fill_height_i32 = fill.height as i32;
4973 let mut alpha = vec![0.0f32; (out_width * out_height) as usize];
4974
4975 for out_y in 0..out_height {
4976 let oy = out_y - radius;
4977 for out_x in 0..out_width {
4978 let ox = out_x - radius;
4979 let base_alpha =
4980 if ox >= 0 && oy >= 0 && ox < fill_width_i32 && oy < fill_height_i32 {
4981 fill.alpha[oy as usize * fill.width + ox as usize]
4982 } else {
4983 0.0
4984 };
4985
4986 let mut dilated_alpha = 0.0f32;
4987 for (dx, dy) in &offsets {
4988 let sx = ox + dx;
4989 let sy = oy + dy;
4990 if sx < 0 || sy < 0 || sx >= fill_width_i32 || sy >= fill_height_i32 {
4991 continue;
4992 }
4993 let sample = fill.alpha[sy as usize * fill.width + sx as usize];
4994 if sample > dilated_alpha {
4995 dilated_alpha = sample;
4996 if dilated_alpha >= 0.999 {
4997 break;
4998 }
4999 }
5000 }
5001 alpha[out_y as usize * out_width as usize + out_x as usize] =
5002 (dilated_alpha - base_alpha).max(0.0);
5003 }
5004 }
5005
5006 GlyphMask {
5007 alpha: Arc::from(alpha),
5008 width: out_width as usize,
5009 height: out_height as usize,
5010 origin_x: fill.origin_x - radius,
5011 origin_y: fill.origin_y - radius,
5012 }
5013 }
5014
5015 fn rasterize_reference_dilation_stroke(
5016 text: &str,
5017 rect: Rect,
5018 font_size: f32,
5019 stroke_width: f32,
5020 font: &impl Font,
5021 ) -> ImageBitmap {
5022 let width = rect.width.ceil().max(1.0) as u32;
5023 let height = rect.height.ceil().max(1.0) as u32;
5024 let mut canvas = vec![[0.0f32; 4]; (width * height) as usize];
5025
5026 let metrics = vertical_metrics(font, font_size);
5027 let baseline = baseline_y_for_line_box(metrics, font_size * 1.4);
5028 for glyph in layout_line_glyphs(font, text, font_size, point(0.0, baseline)) {
5029 let Some((outlined, bounds)) = outline_glyph_with_bounds(font, &glyph) else {
5030 continue;
5031 };
5032 let Some(fill) = build_fill_mask(&outlined, bounds) else {
5033 continue;
5034 };
5035 let reference = reference_dilation_stroke_mask(&fill, stroke_width);
5036 draw_mask_glyph(
5037 &mut canvas,
5038 width,
5039 height,
5040 &reference,
5041 &Brush::solid(Color::WHITE),
5042 1.0,
5043 rect,
5044 );
5045 }
5046
5047 let mut rgba = vec![0u8; canvas.len() * 4];
5048 for (index, pixel) in canvas.iter().enumerate() {
5049 let base = index * 4;
5050 rgba[base] = (pixel[0].clamp(0.0, 1.0) * 255.0).round() as u8;
5051 rgba[base + 1] = (pixel[1].clamp(0.0, 1.0) * 255.0).round() as u8;
5052 rgba[base + 2] = (pixel[2].clamp(0.0, 1.0) * 255.0).round() as u8;
5053 rgba[base + 3] = (pixel[3].clamp(0.0, 1.0) * 255.0).round() as u8;
5054 }
5055 ImageBitmap::from_rgba8(width, height, rgba).expect("reference dilation image")
5056 }
5057
5058 fn test_font() -> ab_glyph::FontRef<'static> {
5059 ab_glyph::FontRef::try_from_slice(include_bytes!("../assets/NotoSansMerged.ttf"))
5060 .expect("font")
5061 }
5062
5063 fn test_software_font() -> SoftwareTextFont {
5064 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansMerged.ttf").to_vec())
5065 .expect("font")
5066 }
5067
5068 #[test]
5069 fn software_text_font_rejects_invalid_bytes() {
5070 assert!(SoftwareTextFont::from_bytes(vec![0, 1, 2, 3]).is_err());
5071 }
5072
5073 #[test]
5074 fn default_software_text_font_has_no_process_global_cache() {
5075 let source = include_str!("software_text_raster.rs");
5076 let once_lock = ["Once", "Lock"].concat();
5077 let cached_default = ["static ", "FONT"].concat();
5078 let default_font_fn = ["fn ", "default_font()"].concat();
5079
5080 assert!(
5081 !source.contains(&cached_default)
5082 && !source.contains(&default_font_fn)
5083 && !source.contains(&once_lock),
5084 "default software text font construction must be explicit renderer/app-owned state, not a process-global cache"
5085 );
5086 }
5087
5088 #[test]
5089 fn software_text_measurer_empty_font_set_uses_deterministic_fallback_without_panicking() {
5090 let measurer = SoftwareTextMeasurer::from_font_set(SoftwareTextFontSet::empty(), 4);
5091 let style = TextStyle {
5092 span_style: SpanStyle {
5093 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5094 ..Default::default()
5095 },
5096 ..Default::default()
5097 };
5098 let text = AnnotatedString::from("ab\nc");
5099
5100 let metrics = measurer.measure(&text, &style);
5101 assert_eq!(metrics.line_count, 2);
5102 assert!(metrics.width > 0.0);
5103 assert!(metrics.height >= metrics.line_height * 2.0);
5104
5105 let cursor_x = measurer.get_cursor_x_for_offset(&text, &style, 2);
5106 assert!(cursor_x > 0.0);
5107 let second_line_offset =
5108 measurer.get_offset_for_position(&text, &style, 0.0, metrics.line_height);
5109 assert!(
5110 second_line_offset >= "ab\n".len(),
5111 "fallback hit testing should resolve into the second line: {second_line_offset}"
5112 );
5113
5114 let layout = measurer.layout(&text, &style);
5115 assert_eq!(layout.lines.len(), 2);
5116 assert_eq!(layout.glyph_layouts().len(), 3);
5117 }
5118
5119 #[test]
5120 fn software_text_metrics_layout_and_cursor_share_font_backend() {
5121 let font = test_software_font();
5122 let style = TextStyle {
5123 span_style: SpanStyle {
5124 font_size: cranpose_ui::text::TextUnit::Sp(18.0),
5125 ..Default::default()
5126 },
5127 ..Default::default()
5128 };
5129 let text = "Text\nBackend";
5130
5131 let metrics = measure_text_with_font(text, &style, 18.0, &font);
5132 let layout = layout_text_with_font(text, &style, &font);
5133
5134 assert!(metrics.width > 0.0);
5135 assert_eq!(metrics.line_count, 2);
5136 assert_eq!(layout.lines.len(), 2);
5137 assert_eq!(layout.height, metrics.height);
5138 assert!(layout.glyph_layouts().len() >= "TextBackend".len());
5139
5140 let offset =
5141 text_offset_for_position_with_font(text, &style, 0.0, metrics.line_height, &font);
5142 assert!(
5143 offset >= "Text\n".len(),
5144 "second-line hit testing should return a byte offset on the second line: {offset}"
5145 );
5146 let cursor_x = cursor_x_for_offset_with_font(text, &style, "Text".len(), &font);
5147 assert!(cursor_x > 0.0);
5148 }
5149
5150 #[test]
5151 fn software_text_metrics_keep_requested_font_size_for_default_font() {
5152 let font = default_software_text_font().expect("bundled default test font");
5153 let style = TextStyle {
5154 span_style: SpanStyle {
5155 font_size: cranpose_ui::text::TextUnit::Sp(14.0),
5156 ..Default::default()
5157 },
5158 ..Default::default()
5159 };
5160
5161 let metrics = measure_text_with_font("Counter App", &style, 14.0, &font);
5162 assert!(
5163 (metrics.width - 83.16).abs() < 0.05 && (metrics.height - 19.6).abs() < 0.05,
5164 "14sp demo text must use font em metrics, not ab_glyph height units: {metrics:?}"
5165 );
5166 }
5167
5168 #[test]
5169 fn software_text_synthesizes_missing_bold_weight() {
5170 let font = test_software_font();
5171 let normal_style = TextStyle {
5172 span_style: SpanStyle {
5173 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5174 ..Default::default()
5175 },
5176 ..Default::default()
5177 };
5178 let bold_style = TextStyle {
5179 span_style: SpanStyle {
5180 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5181 font_weight: Some(FontWeight::BOLD),
5182 ..Default::default()
5183 },
5184 ..Default::default()
5185 };
5186 let no_synthesis_style = TextStyle {
5187 span_style: SpanStyle {
5188 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5189 font_weight: Some(FontWeight::BOLD),
5190 font_synthesis: Some(FontSynthesis::None),
5191 ..Default::default()
5192 },
5193 ..Default::default()
5194 };
5195
5196 let normal = measure_text_with_font("Save Raster WebP", &normal_style, 20.0, &font);
5197 let synthesized = measure_text_with_font("Save Raster WebP", &bold_style, 20.0, &font);
5198 let disabled = measure_text_with_font("Save Raster WebP", &no_synthesis_style, 20.0, &font);
5199
5200 assert!(
5201 synthesized.width > normal.width * 1.04,
5202 "bold fallback should synthesize heavier advances: normal={normal:?} synthesized={synthesized:?}"
5203 );
5204 assert!(
5205 (disabled.width - normal.width).abs() < 0.01,
5206 "explicit FontSynthesis::None should preserve regular metrics: normal={normal:?} disabled={disabled:?}"
5207 );
5208 }
5209
5210 #[test]
5211 fn rasterized_synthetic_bold_adds_ink_without_changing_line_box() {
5212 let font = test_software_font();
5213 let normal_style = TextStyle {
5214 span_style: SpanStyle {
5215 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5216 ..Default::default()
5217 },
5218 ..Default::default()
5219 };
5220 let bold_style = TextStyle {
5221 span_style: SpanStyle {
5222 font_size: cranpose_ui::text::TextUnit::Sp(20.0),
5223 font_weight: Some(FontWeight::BOLD),
5224 ..Default::default()
5225 },
5226 ..Default::default()
5227 };
5228 let normal_metrics = measure_text_with_font("Composer", &normal_style, 20.0, &font);
5229 let bold_metrics = measure_text_with_font("Composer", &bold_style, 20.0, &font);
5230
5231 let normal = rasterize_text_to_image(
5232 "Composer",
5233 Rect {
5234 x: 0.0,
5235 y: 0.0,
5236 width: normal_metrics.width.ceil(),
5237 height: normal_metrics.height.ceil(),
5238 },
5239 &normal_style,
5240 Color::WHITE,
5241 20.0,
5242 1.0,
5243 &font,
5244 )
5245 .expect("normal text image");
5246 let bold = rasterize_text_to_image(
5247 "Composer",
5248 Rect {
5249 x: 0.0,
5250 y: 0.0,
5251 width: bold_metrics.width.ceil(),
5252 height: bold_metrics.height.ceil(),
5253 },
5254 &bold_style,
5255 Color::WHITE,
5256 20.0,
5257 1.0,
5258 &font,
5259 )
5260 .expect("bold text image");
5261
5262 assert_eq!(bold.height(), normal.height());
5263 assert!(
5264 count_ink_pixels(&bold) > count_ink_pixels(&normal),
5265 "synthetic bold should increase rasterized ink coverage"
5266 );
5267 }
5268
5269 #[test]
5270 fn software_text_synthesizes_missing_italic_style() {
5271 let font = test_software_font();
5272 let normal_style = TextStyle {
5273 span_style: SpanStyle {
5274 font_size: cranpose_ui::text::TextUnit::Sp(36.0),
5275 ..Default::default()
5276 },
5277 ..Default::default()
5278 };
5279 let italic_style = TextStyle {
5280 span_style: SpanStyle {
5281 font_size: cranpose_ui::text::TextUnit::Sp(36.0),
5282 font_style: Some(FontStyle::Italic),
5283 ..Default::default()
5284 },
5285 ..Default::default()
5286 };
5287 let no_synthesis_style = TextStyle {
5288 span_style: SpanStyle {
5289 font_size: cranpose_ui::text::TextUnit::Sp(36.0),
5290 font_style: Some(FontStyle::Italic),
5291 font_synthesis: Some(FontSynthesis::None),
5292 ..Default::default()
5293 },
5294 ..Default::default()
5295 };
5296
5297 let normal_metrics = measure_text_with_font("Italic", &normal_style, 36.0, &font);
5298 let italic_metrics = measure_text_with_font("Italic", &italic_style, 36.0, &font);
5299 let disabled_metrics = measure_text_with_font("Italic", &no_synthesis_style, 36.0, &font);
5300
5301 assert!(
5302 italic_metrics.width > normal_metrics.width + 6.0,
5303 "italic fallback should reserve slanted visual overhang: normal={normal_metrics:?} italic={italic_metrics:?}"
5304 );
5305 assert!(
5306 (disabled_metrics.width - normal_metrics.width).abs() < 0.01,
5307 "explicit FontSynthesis::None should preserve regular metrics: normal={normal_metrics:?} disabled={disabled_metrics:?}"
5308 );
5309
5310 let normal = rasterize_text_to_image(
5311 "Italic",
5312 Rect {
5313 x: 0.0,
5314 y: 0.0,
5315 width: normal_metrics.width.ceil(),
5316 height: normal_metrics.height.ceil(),
5317 },
5318 &normal_style,
5319 Color::WHITE,
5320 36.0,
5321 1.0,
5322 &font,
5323 )
5324 .expect("normal text image");
5325 let italic = rasterize_text_to_image(
5326 "Italic",
5327 Rect {
5328 x: 0.0,
5329 y: 0.0,
5330 width: italic_metrics.width.ceil(),
5331 height: italic_metrics.height.ceil(),
5332 },
5333 &italic_style,
5334 Color::WHITE,
5335 36.0,
5336 1.0,
5337 &font,
5338 )
5339 .expect("italic text image");
5340 let disabled = rasterize_text_to_image(
5341 "Italic",
5342 Rect {
5343 x: 0.0,
5344 y: 0.0,
5345 width: disabled_metrics.width.ceil(),
5346 height: disabled_metrics.height.ceil(),
5347 },
5348 &no_synthesis_style,
5349 Color::WHITE,
5350 36.0,
5351 1.0,
5352 &font,
5353 )
5354 .expect("disabled italic text image");
5355
5356 assert_eq!(
5357 normal.pixels(),
5358 disabled.pixels(),
5359 "FontSynthesis::None must not synthesize oblique glyphs"
5360 );
5361 assert!(
5362 vertical_slant_delta(&italic) > vertical_slant_delta(&normal) + 2.0,
5363 "synthetic italic should visibly lean top ink to the right"
5364 );
5365 }
5366
5367 #[test]
5368 fn rasterized_default_text_fills_expected_visual_height() {
5369 let font = default_software_text_font().expect("bundled default test font");
5370 let style = TextStyle {
5371 span_style: SpanStyle {
5372 font_size: cranpose_ui::text::TextUnit::Sp(14.0),
5373 ..Default::default()
5374 },
5375 ..Default::default()
5376 };
5377 let metrics = measure_text_with_font("Counter App", &style, 14.0, &font);
5378 let image = rasterize_text_to_image(
5379 "Counter App",
5380 Rect {
5381 x: 0.0,
5382 y: 0.0,
5383 width: metrics.width.ceil(),
5384 height: metrics.height.ceil(),
5385 },
5386 &style,
5387 Color::WHITE,
5388 14.0,
5389 1.0,
5390 &font,
5391 )
5392 .expect("text image");
5393 let (top, bottom) = ink_y_range(&image).expect("text should contain ink");
5394 let ink_height = bottom - top;
5395
5396 assert!(
5397 ink_height >= 13,
5398 "14sp default text ink should keep visual height parity with the WGPU baseline: top={top} bottom={bottom} image={}x{}",
5399 image.width(),
5400 image.height()
5401 );
5402 }
5403
5404 #[test]
5405 fn software_text_font_selection_preserves_first_complete_default_face() {
5406 let regular =
5407 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansMerged.ttf").to_vec())
5408 .expect("regular test font should load");
5409 let font = software_text_font_from_fonts_or_default(&[
5410 include_bytes!("../assets/NotoSansMerged.ttf"),
5411 include_bytes!("../assets/NotoSansBold.ttf"),
5412 include_bytes!("../assets/TwemojiMozilla.ttf"),
5413 ])
5414 .expect("font selection should resolve a test font");
5415 let style = TextStyle {
5416 span_style: SpanStyle {
5417 font_size: cranpose_ui::text::TextUnit::Sp(18.0),
5418 ..Default::default()
5419 },
5420 ..Default::default()
5421 };
5422
5423 let regular_metrics = measure_text_with_font("UNDER", &style, 18.0, ®ular);
5424 let metrics = measure_text_with_font("UNDER", &style, 18.0, &font);
5425 assert!(
5426 (metrics.width - regular_metrics.width).abs() < 0.01,
5427 "font selection should keep the declared regular face for default text: selected={metrics:?}, regular={regular_metrics:?}"
5428 );
5429 }
5430
5431 #[test]
5432 fn software_text_font_resolution_reuses_cached_font_score() {
5433 let font = test_software_font();
5434 assert!(
5435 font.score.is_complete_default_face(),
5436 "test font should cache complete Latin coverage at load time: supported={} width={}",
5437 font.score.supported_latin_chars,
5438 font.score.latin_sample_width
5439 );
5440
5441 let fonts = SoftwareTextFontSet::from_font(font.clone());
5442 let resolved = fonts
5443 .resolve(&TextStyle {
5444 span_style: SpanStyle {
5445 font_weight: Some(FontWeight::BOLD),
5446 ..Default::default()
5447 },
5448 ..Default::default()
5449 })
5450 .expect("font set should resolve a test font");
5451
5452 assert_eq!(
5453 resolved.score.supported_latin_chars,
5454 font.score.supported_latin_chars
5455 );
5456 assert_eq!(
5457 resolved.score.latin_sample_width,
5458 font.score.latin_sample_width
5459 );
5460 }
5461
5462 #[test]
5463 fn software_text_font_set_resolves_requested_weight() {
5464 let fonts = software_text_font_set_from_fonts_or_default(&[
5465 include_bytes!("../assets/NotoSansMerged.ttf"),
5466 include_bytes!("../assets/NotoSansBold.ttf"),
5467 include_bytes!("../assets/TwemojiMozilla.ttf"),
5468 ]);
5469 let regular = fonts
5470 .resolve(&TextStyle::default())
5471 .expect("font set should resolve regular test font");
5472 let bold_style = TextStyle {
5473 span_style: SpanStyle {
5474 font_weight: Some(FontWeight::BOLD),
5475 ..Default::default()
5476 },
5477 ..Default::default()
5478 };
5479 let bold = fonts
5480 .resolve(&bold_style)
5481 .expect("font set should resolve bold test font");
5482
5483 assert_eq!(regular.weight(), FontWeight::NORMAL);
5484 assert_eq!(bold.weight(), FontWeight::BOLD);
5485
5486 let regular_metrics =
5487 measure_text_with_font("Counter App", &TextStyle::default(), 18.0, regular);
5488 let bold_metrics = measure_text_with_font("Counter App", &bold_style, 18.0, bold);
5489 assert!(
5490 bold_metrics.width > regular_metrics.width,
5491 "bold face resolution should affect real text metrics: regular={regular_metrics:?} bold={bold_metrics:?}"
5492 );
5493 }
5494
5495 fn registered_face(family: &FontFamily, weight: FontWeight) -> SoftwareTextFont {
5496 SoftwareTextFont::from_registered_bytes(
5497 family,
5498 weight,
5499 FontStyle::Normal,
5500 include_bytes!("../assets/NotoSansMerged.ttf").to_vec(),
5501 )
5502 .expect("registered test face")
5503 }
5504
5505 fn style_naming(family: &FontFamily) -> TextStyle {
5506 TextStyle {
5507 span_style: SpanStyle {
5508 font_family: Some(family.clone()),
5509 ..Default::default()
5510 },
5511 ..Default::default()
5512 }
5513 }
5514
5515 fn unregistered_face() -> SoftwareTextFont {
5516 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansBold.ttf").to_vec())
5517 .expect("unregistered test face")
5518 }
5519
5520 #[test]
5521 fn a_named_family_resolves_the_face_registered_under_it() {
5522 let family = FontFamily::named("Game UI");
5525 let fonts = SoftwareTextFontSet::from_faces(vec![
5526 unregistered_face(),
5527 registered_face(&family, FontWeight::NORMAL),
5528 ]);
5529
5530 let resolved = fonts
5531 .resolve(&style_naming(&family))
5532 .expect("registered face");
5533 assert_eq!(
5534 resolved.registered_family(),
5535 Some(FontFamilyKey::of(&family))
5536 );
5537 }
5538
5539 #[test]
5540 fn a_file_backed_family_never_resolves_a_face_filed_under_another_one() {
5541 let mine = FontFamily::loaded_typeface_path("/fonts/Mine.ttf");
5542 let theirs = FontFamily::loaded_typeface_path("/fonts/Theirs.ttf");
5543 let fallback =
5544 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansMerged.ttf").to_vec())
5545 .expect("fallback test face");
5546 let theirs_face = SoftwareTextFont::from_registered_bytes(
5547 &theirs,
5548 FontWeight::BOLD,
5549 FontStyle::Normal,
5550 include_bytes!("../assets/NotoSansBold.ttf").to_vec(),
5551 )
5552 .expect("registered test face");
5553 let fonts = SoftwareTextFontSet::from_faces(vec![fallback.clone(), theirs_face]);
5554
5555 assert_eq!(
5556 fonts
5557 .resolve(&style_naming(&mine))
5558 .expect("fallback face")
5559 .content_hash(),
5560 fallback.content_hash(),
5561 "an unregistered family must fall back rather than borrow someone else's face"
5562 );
5563 assert_eq!(
5564 fonts
5565 .resolve(&style_naming(&theirs))
5566 .expect("registered face")
5567 .registered_family(),
5568 Some(FontFamilyKey::of(&theirs)),
5569 "the family that was registered still resolves to its own face"
5570 );
5571 }
5572
5573 #[test]
5574 fn a_generic_family_only_constrains_the_set_once_a_face_is_registered_for_it() {
5575 let bold_sans_serif = TextStyle {
5576 span_style: SpanStyle {
5577 font_family: Some(FontFamily::SansSerif),
5578 font_weight: Some(FontWeight::BOLD),
5579 ..Default::default()
5580 },
5581 ..Default::default()
5582 };
5583
5584 let unclaimed = software_text_font_set_from_fonts_or_default(&[
5587 include_bytes!("../assets/NotoSansMerged.ttf"),
5588 include_bytes!("../assets/NotoSansBold.ttf"),
5589 ]);
5590 assert_eq!(
5591 unclaimed
5592 .resolve(&bold_sans_serif)
5593 .expect("bold face")
5594 .weight(),
5595 FontWeight::BOLD
5596 );
5597
5598 let claimed = SoftwareTextFontSet::from_faces(vec![
5601 SoftwareTextFont::from_bytes(include_bytes!("../assets/NotoSansBold.ttf").to_vec())
5602 .expect("bold test face"),
5603 registered_face(&FontFamily::SansSerif, FontWeight::NORMAL),
5604 ]);
5605 let resolved = claimed.resolve(&bold_sans_serif).expect("system face");
5606 assert_eq!(
5607 resolved.registered_family(),
5608 Some(FontFamilyKey::of(&FontFamily::SansSerif))
5609 );
5610 }
5611
5612 #[test]
5613 fn an_app_supplied_family_measures_once_and_is_served_from_the_metrics_cache() {
5614 let family = FontFamily::named("Game UI");
5615 let measurer = SoftwareTextMeasurer::from_font_set(
5616 SoftwareTextFontSet::from_faces(vec![registered_face(&family, FontWeight::NORMAL)]),
5617 64,
5618 );
5619 let style = style_naming(&family);
5620 let text = AnnotatedString::from("SCORE 1234");
5621
5622 let first = measurer.measure(&text, &style);
5623 let stats_after_first = measurer.lock_cache().glyph_metrics.stats();
5624 for _ in 0..60 {
5625 assert_eq!(measurer.measure(&text, &style), first);
5626 }
5627
5628 assert_eq!(
5629 measurer.lock_cache().glyph_metrics.stats(),
5630 stats_after_first,
5631 "repeat frames of an unchanged string must not re-shape against the app face"
5632 );
5633 }
5634
5635 #[test]
5636 fn a_font_size_animation_measures_each_glyph_once_rather_than_once_per_size() {
5637 let font = default_software_text_font().expect("bundled default test font");
5638 let measurer = SoftwareTextMeasurer::new(font, 64);
5639 let text = AnnotatedString::from("Scaling list row");
5640
5641 let sized = |size: f32| TextStyle {
5642 span_style: SpanStyle {
5643 font_size: cranpose_ui::text::TextUnit::Sp(size),
5644 ..Default::default()
5645 },
5646 ..Default::default()
5647 };
5648
5649 let first = measurer.measure(&text, &sized(14.0));
5651 let after_first = measurer.lock_cache().glyph_metrics.stats();
5652
5653 for step in 0..120 {
5657 let size = 14.0 + step as f32 * 0.137;
5658 let measured = measurer.measure(&text, &sized(size));
5659 assert!(
5660 measured.width > 0.0,
5661 "a scaled measurement must still produce a width"
5662 );
5663 }
5664
5665 let after_scaling = measurer.lock_cache().glyph_metrics.stats();
5669 assert_eq!(
5670 (after_scaling.glyph_misses, after_scaling.kern_misses),
5671 (after_first.glyph_misses, after_first.kern_misses),
5672 "measuring the same glyphs at a new size must not re-read the font: {after_scaling:?}"
5673 );
5674 assert!(
5675 after_scaling.glyph_hits > after_first.glyph_hits,
5676 "the scaled measurements must have come from the cache"
5677 );
5678
5679 let single = measurer.measure(&AnnotatedString::from("W"), &sized(20.0));
5681 let double = measurer.measure(&AnnotatedString::from("W"), &sized(40.0));
5682 let ratio = double.width / single.width.max(f32::EPSILON);
5683 assert!(
5684 (ratio - 2.0).abs() < 0.01,
5685 "advances must scale with the font size: {single:?} -> {double:?} (ratio {ratio})"
5686 );
5687 let _ = first;
5688 }
5689
5690 #[test]
5691 fn software_text_metrics_use_largest_annotated_span_font_size() {
5692 let font = default_software_text_font().expect("bundled default test font");
5693 let text = AnnotatedString::builder()
5694 .push_style(SpanStyle {
5695 font_size: cranpose_ui::text::TextUnit::Sp(30.0),
5696 ..Default::default()
5697 })
5698 .append("BIG ")
5699 .pop()
5700 .push_style(SpanStyle {
5701 font_size: cranpose_ui::text::TextUnit::Sp(10.0),
5702 ..Default::default()
5703 })
5704 .append("small")
5705 .pop()
5706 .to_annotated_string();
5707
5708 let metrics = measure_annotated_text_with_font(&text, &TextStyle::default(), 14.0, &font);
5709
5710 assert!(
5711 metrics.height >= 30.0,
5712 "rich text metrics must include the largest span height: {metrics:?}"
5713 );
5714 assert!(
5715 metrics.width > 48.0,
5716 "rich text metrics should measure run widths at their span sizes: {metrics:?}"
5717 );
5718 }
5719
5720 #[test]
5721 fn software_text_line_height_matches_full_measurement_without_width_layout() {
5722 let measurer = SoftwareTextMeasurer::new(
5723 default_software_text_font().expect("bundled default test font"),
5724 8,
5725 );
5726 let text = AnnotatedString::builder()
5727 .append("normal ")
5728 .push_style(SpanStyle {
5729 font_size: cranpose_ui::text::TextUnit::Sp(32.0),
5730 ..Default::default()
5731 })
5732 .append("large")
5733 .pop()
5734 .append("\nsecond line")
5735 .to_annotated_string();
5736 let style = TextStyle::default();
5737
5738 let measured = measurer.measure(&text, &style);
5739 let line_height = measurer.line_height(&text, &style);
5740
5741 assert_eq!(line_height, measured.line_height);
5742 assert!(
5743 line_height > measurer.line_height(&AnnotatedString::from("normal"), &style),
5744 "span font size should affect fast line-height lookup"
5745 );
5746 }
5747
5748 #[test]
5749 fn solid_text_atlas_line_advance_matches_measured_line_height() {
5750 let font = default_software_text_font().expect("bundled default test font");
5751 let fonts = SoftwareTextFontSet::from_font(font);
5752 let style = TextStyle::default();
5753 let text = AnnotatedString::from("A\nA\nA\nA");
5754 let font_size = style.resolve_font_size(14.0);
5755 let metrics = measure_annotated_text_with_font_set(&text, &style, font_size, &fonts);
5756 let rect = Rect {
5757 x: 0.0,
5758 y: 0.0,
5759 width: 120.0,
5760 height: metrics.height,
5761 };
5762 let mut glyph_cache = SoftwareGlyphRasterCache::with_capacity_at_least_one(16);
5763 let mut run = Vec::new();
5764
5765 collect_solid_text_atlas_run(
5766 &text,
5767 rect,
5768 &style,
5769 Color(1.0, 1.0, 1.0, 1.0),
5770 font_size,
5771 1.0,
5772 &fonts,
5773 &mut glyph_cache,
5774 &mut run,
5775 )
5776 .expect("atlas-compatible text");
5777
5778 let mut glyph_y: Vec<i32> = run.iter().map(|glyph| glyph.placement().y).collect();
5779 glyph_y.sort_unstable();
5780 glyph_y.dedup();
5781 assert_eq!(glyph_y.len(), 4);
5782 for window in glyph_y.windows(2) {
5783 let advance = (window[1] - window[0]) as f32;
5784 assert!(
5785 (advance - metrics.line_height).abs() <= 1.0,
5786 "glyph advance {advance} should match measured line height {}",
5787 metrics.line_height
5788 );
5789 }
5790 }
5791
5792 #[test]
5793 fn software_text_metrics_cache_keys_include_span_styles() {
5794 let measurer = SoftwareTextMeasurer::new(
5795 default_software_text_font().expect("bundled default test font"),
5796 8,
5797 );
5798 let plain = AnnotatedString::from("BIG small");
5799 let rich = AnnotatedString::builder()
5800 .push_style(SpanStyle {
5801 font_size: cranpose_ui::text::TextUnit::Sp(30.0),
5802 ..Default::default()
5803 })
5804 .append("BIG ")
5805 .pop()
5806 .append("small")
5807 .to_annotated_string();
5808
5809 let plain_metrics = measurer.measure(&plain, &TextStyle::default());
5810 let rich_metrics = measurer.measure(&rich, &TextStyle::default());
5811
5812 assert!(
5813 rich_metrics.height > plain_metrics.height,
5814 "cached plain text metrics must not be reused for styled text: plain={plain_metrics:?} rich={rich_metrics:?}"
5815 );
5816 }
5817
5818 #[test]
5819 fn software_text_metrics_cache_recovers_after_poison() {
5820 let measurer = SoftwareTextMeasurer::new(
5821 default_software_text_font().expect("bundled default test font"),
5822 8,
5823 );
5824 let text = AnnotatedString::from("Recovered text metrics");
5825
5826 let poison_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5827 let _guard = measurer
5828 .cache
5829 .lock()
5830 .unwrap_or_else(|poisoned| poisoned.into_inner());
5831 panic!("poison software text metrics cache for recovery test");
5832 }));
5833
5834 assert!(poison_result.is_err());
5835
5836 let metrics = measurer.measure(&text, &TextStyle::default());
5837 assert!(metrics.width > 0.0);
5838 assert!(metrics.height > 0.0);
5839
5840 let subset =
5841 measurer.measure_subsequence(&text, 0.."Recovered".len(), &TextStyle::default());
5842 assert!(subset.width > 0.0);
5843 assert!(subset.width < metrics.width);
5844 }
5845
5846 #[test]
5847 fn software_text_prefix_widths_match_subsequence_measurement() {
5848 let measurer = SoftwareTextMeasurer::new(
5849 default_software_text_font().expect("bundled default test font"),
5850 8,
5851 );
5852 let style = TextStyle {
5853 span_style: SpanStyle {
5854 font_size: cranpose_ui::text::TextUnit::Sp(18.0),
5855 ..Default::default()
5856 },
5857 ..Default::default()
5858 };
5859 let text = AnnotatedString::from("Hello Prefix Widths");
5860 let widths = measurer
5861 .measure_line_prefix_widths(&text, 0..text.text.len(), &style)
5862 .expect("uniform line should expose prefix widths");
5863
5864 let start = "Hello ".len();
5865 let end = "Hello Prefix".len();
5866 let expected = measurer
5867 .measure_subsequence(&text, start..end, &style)
5868 .width;
5869 let actual = widths
5870 .width_for_char_range(6, 12)
5871 .expect("valid char range");
5872
5873 assert!(
5874 (actual - expected).abs() < 0.01,
5875 "prefix width should match exact subsequence width: actual={actual}, expected={expected}"
5876 );
5877 }
5878
5879 #[test]
5880 fn software_text_line_width_and_prefix_width_share_cached_plan() {
5881 let measurer = SoftwareTextMeasurer::new(
5882 default_software_text_font().expect("bundled default test font"),
5883 8,
5884 );
5885 let style = TextStyle::default();
5886 let text = AnnotatedString::from("shared prefix plan ".repeat(32).as_str());
5887 let line_range = 0..text.text.len();
5888
5889 let width = measurer
5890 .measure_line_width(&text, line_range.clone(), &style)
5891 .expect("software text should expose a line width");
5892 let stats_after_width = {
5893 let cache = measurer.lock_cache();
5894 assert_eq!(cache.line_prefix_widths.len(), 1);
5895 cache.glyph_metrics.stats()
5896 };
5897
5898 let widths = measurer
5899 .measure_line_prefix_widths(&text, line_range, &style)
5900 .expect("line width probe should cache the prefix plan");
5901 let stats_after_prefix = measurer.lock_cache().glyph_metrics.stats();
5902
5903 assert_eq!(stats_after_prefix, stats_after_width);
5904 assert!(
5905 (width - widths.width_for_char_range(0, widths.char_count()).unwrap()).abs() < 0.01,
5906 "cached line-width probe and prefix plan must agree"
5907 );
5908 }
5909
5910 #[test]
5911 fn software_text_glyph_metrics_cache_reuses_common_glyphs_across_unique_lines() {
5912 let measurer = SoftwareTextMeasurer::new(
5913 default_software_text_font().expect("bundled default test font"),
5914 8,
5915 );
5916 let style = TextStyle::default();
5917 let first = AnnotatedString::from("algorithm data structure ".repeat(24).as_str());
5918 let second =
5919 AnnotatedString::from("algorithmic structures repeat data ".repeat(24).as_str());
5920
5921 measurer
5922 .measure_line_prefix_widths(&first, 0..first.text.len(), &style)
5923 .expect("first unique line should measure");
5924 let stats_after_first = measurer.lock_cache().glyph_metrics.stats();
5925
5926 measurer
5927 .measure_line_prefix_widths(&second, 0..second.text.len(), &style)
5928 .expect("second unique line should measure");
5929 let stats_after_second = measurer.lock_cache().glyph_metrics.stats();
5930
5931 assert!(
5932 stats_after_second.glyph_hits > stats_after_first.glyph_hits,
5933 "unique markdown rows should reuse retained glyph metrics: first={stats_after_first:?} second={stats_after_second:?}"
5934 );
5935 assert!(
5936 stats_after_second.kern_hits > stats_after_first.kern_hits,
5937 "unique markdown rows should reuse retained kerning metrics: first={stats_after_first:?} second={stats_after_second:?}"
5938 );
5939 }
5940
5941 #[test]
5942 fn rasterized_gradient_text_shows_color_transition() {
5943 let font = test_font();
5944 let plain_style = TextStyle::default();
5947 let probe = rasterize_text_to_image_with_font(
5948 "MMMMMMMM",
5949 Rect {
5950 x: 0.0,
5951 y: 0.0,
5952 width: 320.0,
5953 height: 96.0,
5954 },
5955 &plain_style,
5956 Color::WHITE,
5957 48.0,
5958 1.0,
5959 &font,
5960 )
5961 .expect("probe image");
5962 let (ink_x_min, ink_x_max) = ink_x_range(&probe).expect("probe must contain ink");
5963 let gradient_end = ink_x_max as f32;
5964
5965 let style = TextStyle {
5966 span_style: SpanStyle {
5967 brush: Some(Brush::linear_gradient_range(
5968 vec![Color::RED, Color::BLUE],
5969 Point::new(0.0, 0.0),
5970 Point::new(gradient_end, 0.0),
5971 )),
5972 ..Default::default()
5973 },
5974 ..Default::default()
5975 };
5976
5977 let image = rasterize_text_to_image_with_font(
5978 "MMMMMMMM",
5979 Rect {
5980 x: 0.0,
5981 y: 0.0,
5982 width: 320.0,
5983 height: 96.0,
5984 },
5985 &style,
5986 Color::WHITE,
5987 48.0,
5988 1.0,
5989 &font,
5990 )
5991 .expect("rasterized image");
5992
5993 let ink_span = ink_x_max.saturating_sub(ink_x_min).max(1);
5994 let left_end = ink_x_min + ink_span * 3 / 10;
5995 let right_start = ink_x_max.saturating_sub(ink_span * 3 / 10);
5996 let left = average_ink_rgb(&image, ink_x_min, left_end, 8, 90).expect("left ink");
5997 let right = average_ink_rgb(&image, right_start, ink_x_max, 8, 90).expect("right ink");
5998 assert!(
5999 left[0] > left[2] * 1.1,
6000 "left region should be red dominant, got {left:?}"
6001 );
6002 assert!(
6003 right[2] > right[0] * 1.1,
6004 "right region should be blue dominant, got {right:?}"
6005 );
6006 }
6007
6008 #[test]
6009 fn rasterized_stroke_and_fill_ink_coverage_differs() {
6010 let font = test_font();
6011 let fill_style = TextStyle::default();
6012 let stroke_style = TextStyle {
6013 span_style: SpanStyle {
6014 draw_style: Some(TextDrawStyle::Stroke { width: 6.0 }),
6015 ..Default::default()
6016 },
6017 ..Default::default()
6018 };
6019 let rect = Rect {
6020 x: 0.0,
6021 y: 0.0,
6022 width: 320.0,
6023 height: 96.0,
6024 };
6025
6026 let fill = rasterize_text_to_image_with_font(
6027 "MMMMMMMM",
6028 rect,
6029 &fill_style,
6030 Color::WHITE,
6031 48.0,
6032 1.0,
6033 &font,
6034 )
6035 .expect("fill image");
6036 let stroke = rasterize_text_to_image_with_font(
6037 "MMMMMMMM",
6038 rect,
6039 &stroke_style,
6040 Color::WHITE,
6041 48.0,
6042 1.0,
6043 &font,
6044 )
6045 .expect("stroke image");
6046
6047 let fill_ink = count_ink_pixels(&fill);
6048 let stroke_ink = count_ink_pixels(&stroke);
6049 assert_ne!(fill.pixels(), stroke.pixels());
6050 assert!(
6051 fill_ink.abs_diff(stroke_ink) > 300,
6052 "fill/stroke ink coverage should differ; fill={fill_ink}, stroke={stroke_ink}"
6053 );
6054 }
6055
6056 #[test]
6057 fn stroke_path_uses_miter_join_for_acute_apexes() {
6058 let font = test_font();
6059 let fill_style = TextStyle::default();
6060 let stroke_width = 12.0;
6061 let stroke_style = TextStyle {
6062 span_style: SpanStyle {
6063 draw_style: Some(TextDrawStyle::Stroke {
6064 width: stroke_width,
6065 }),
6066 ..Default::default()
6067 },
6068 ..Default::default()
6069 };
6070 let rect = Rect {
6071 x: 0.0,
6072 y: 0.0,
6073 width: 180.0,
6074 height: 140.0,
6075 };
6076
6077 let fill = rasterize_text_to_image_with_font(
6078 "A",
6079 rect,
6080 &fill_style,
6081 Color::WHITE,
6082 110.0,
6083 1.0,
6084 &font,
6085 )
6086 .expect("fill image");
6087 let stroke = rasterize_text_to_image_with_font(
6088 "A",
6089 rect,
6090 &stroke_style,
6091 Color::WHITE,
6092 110.0,
6093 1.0,
6094 &font,
6095 )
6096 .expect("stroke image");
6097
6098 let fill_top = top_ink_row(&fill).expect("fill top row");
6099 let stroke_top = top_ink_row(&stroke).expect("stroke top row");
6100 let reference_dilation =
6101 rasterize_reference_dilation_stroke("A", rect, 110.0, stroke_width, &font);
6102 let reference_top = top_ink_row(&reference_dilation).expect("reference top row");
6103 let extra_extension = fill_top.saturating_sub(stroke_top) as f32;
6104 let half_stroke = stroke_width * 0.5;
6105 assert!(
6106 extra_extension >= half_stroke - 0.25,
6107 "stroke apex should extend by roughly at least half stroke width; fill_top={fill_top}, stroke_top={stroke_top}, half_stroke={half_stroke:.2}"
6108 );
6109 assert!(
6110 stroke.pixels() != reference_dilation.pixels(),
6111 "path stroke should diverge from mask-dilation reference output"
6112 );
6113 assert!(
6114 stroke_top <= reference_top,
6115 "miter stroke should keep acute apex at least as extended as mask-dilation reference; stroke_top={stroke_top}, reference_top={reference_top}"
6116 );
6117 }
6118
6119 #[test]
6120 fn shadow_blur_radius_changes_spread_for_shared_raster_path() {
6121 let font = test_font();
6122 let base_shadow = Shadow {
6123 color: Color(0.0, 0.0, 0.0, 0.9),
6124 offset: Point::new(5.5, 4.25),
6125 blur_radius: 0.0,
6126 };
6127 let hard_shadow_style = TextStyle {
6128 span_style: SpanStyle {
6129 shadow: Some(base_shadow),
6130 ..Default::default()
6131 },
6132 ..Default::default()
6133 };
6134 let blurred_shadow_style = TextStyle {
6135 span_style: SpanStyle {
6136 shadow: Some(Shadow {
6137 blur_radius: 9.0,
6138 ..base_shadow
6139 }),
6140 ..Default::default()
6141 },
6142 ..Default::default()
6143 };
6144 let rect = Rect {
6145 x: 0.0,
6146 y: 0.0,
6147 width: 320.0,
6148 height: 120.0,
6149 };
6150
6151 let hard_shadow = rasterize_text_to_image_with_font(
6152 "Shared shadow",
6153 rect,
6154 &hard_shadow_style,
6155 Color::TRANSPARENT,
6156 48.0,
6157 1.0,
6158 &font,
6159 )
6160 .expect("hard shadow image");
6161 let blurred_shadow = rasterize_text_to_image_with_font(
6162 "Shared shadow",
6163 rect,
6164 &blurred_shadow_style,
6165 Color::TRANSPARENT,
6166 48.0,
6167 1.0,
6168 &font,
6169 )
6170 .expect("blurred shadow image");
6171
6172 let hard_ink = count_ink_pixels(&hard_shadow);
6173 let blurred_ink = count_ink_pixels(&blurred_shadow);
6174 assert_ne!(
6175 hard_shadow.pixels(),
6176 blurred_shadow.pixels(),
6177 "blur radius should change rasterized shadow output"
6178 );
6179 assert!(
6180 blurred_ink > hard_ink,
6181 "blurred shadow should spread to more pixels; hard={hard_ink}, blurred={blurred_ink}"
6182 );
6183 }
6184
6185 #[test]
6186 fn text_motion_changes_fractional_shadow_sampling() {
6187 let font = test_font();
6188 let base_shadow = Shadow {
6189 color: Color(0.0, 0.0, 0.0, 0.9),
6190 offset: Point::new(3.35, 2.65),
6191 blur_radius: 6.0,
6192 };
6193 let static_style = TextStyle {
6194 span_style: SpanStyle {
6195 shadow: Some(base_shadow),
6196 ..Default::default()
6197 },
6198 paragraph_style: cranpose_ui::text::ParagraphStyle {
6199 text_motion: Some(TextMotion::Static),
6200 ..Default::default()
6201 },
6202 };
6203 let animated_style = TextStyle {
6204 span_style: SpanStyle {
6205 shadow: Some(base_shadow),
6206 ..Default::default()
6207 },
6208 paragraph_style: cranpose_ui::text::ParagraphStyle {
6209 text_motion: Some(TextMotion::Animated),
6210 ..Default::default()
6211 },
6212 };
6213 let rect = Rect {
6214 x: 11.35,
6215 y: 7.65,
6216 width: 280.0,
6217 height: 120.0,
6218 };
6219
6220 let static_image = rasterize_text_to_image_with_font(
6221 "Motion shadow",
6222 rect,
6223 &static_style,
6224 Color::TRANSPARENT,
6225 42.0,
6226 1.0,
6227 &font,
6228 )
6229 .expect("static image");
6230 let animated_image = rasterize_text_to_image_with_font(
6231 "Motion shadow",
6232 rect,
6233 &animated_style,
6234 Color::TRANSPARENT,
6235 42.0,
6236 1.0,
6237 &font,
6238 )
6239 .expect("animated image");
6240
6241 assert_ne!(
6242 static_image.pixels(),
6243 animated_image.pixels(),
6244 "TextMotion::Static should quantize shadow placement while Animated keeps fractional sampling"
6245 );
6246 }
6247
6248 #[test]
6249 fn static_text_motion_aligns_glyph_positions_to_pixel_grid() {
6250 let font = test_font();
6251 let base_glyph = layout_line_glyphs(&font, "A", 17.0, point(0.0, 13.37))
6252 .into_iter()
6253 .next()
6254 .expect("glyph");
6255 let static_aligned = align_glyph_for_text_motion(base_glyph, true);
6256 let static_position = static_aligned.position;
6257 assert!(
6258 (static_position.x - static_position.x.round()).abs() < f32::EPSILON,
6259 "static text should snap glyph x to pixel grid"
6260 );
6261 assert!(
6262 (static_position.y - static_position.y.round()).abs() < f32::EPSILON,
6263 "static text should snap glyph y to pixel grid"
6264 );
6265
6266 let animated_source = layout_line_glyphs(&font, "A", 17.0, point(0.0, 13.37))
6267 .into_iter()
6268 .next()
6269 .expect("glyph");
6270 let animated_aligned = align_glyph_for_text_motion(animated_source, false);
6271 let animated_position = animated_aligned.position;
6272 assert!(
6273 (animated_position.y - 13.37).abs() < 1e-3,
6274 "animated text should preserve fractional glyph position"
6275 );
6276 }
6277}