Skip to main content

cranpose_render_common/
software_text_raster.rs

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