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