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