Skip to main content

cranpose_render_common/
software_text_raster.rs

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