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