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