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