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