Skip to main content

cranpose_render_common/
software_text_raster.rs

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