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