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