Skip to main content

cranpose_render_common/
software_text_raster.rs

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