Skip to main content

cranpose_render_common/
software_text_raster.rs

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