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