Skip to main content

cranpose_render_common/
software_text_raster.rs

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