Skip to main content

cranpose_render_common/
software_text_raster.rs

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