Skip to main content

cranpose_render_common/
software_text_raster.rs

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