Skip to main content

cranpose_render_common/
text_glyphs.rs

1//! The glyphs of a laid-out text placed on a renderer's pixels, each with
2//! the paint of the span it draws.
3
4use std::ops::Range;
5
6use cranpose_text::{FontSynthesis, GlyphId, TextLayout, Typeface};
7use cranpose_ui::{
8    Brush, Color,
9    text::{
10        FontStyle, FontWeight, RangeStyle, RenderString, Shadow, SpanBoundaries, SpanStyle,
11        TextDrawStyle, TextMotion, TextStyle,
12    },
13};
14use cranpose_ui_graphics::{Point, whole};
15use smallvec::SmallVec;
16
17use crate::glyph_mask::GlyphRaster;
18
19/// A text and the styles of its ranges.
20#[derive(Clone, Copy, Debug)]
21pub struct StyledTextRef<'a> {
22    /// The text.
23    pub text: &'a str,
24    /// Styles of byte ranges of `text`.
25    pub span_styles: &'a [RangeStyle<SpanStyle>],
26}
27
28impl StyledTextRef<'_> {
29    pub(crate) fn is_empty(&self) -> bool {
30        self.text.is_empty()
31    }
32
33    // The offsets where a span starts or ends, with the text's ends.
34    fn span_boundaries(&self) -> SpanBoundaries {
35        let mut boundaries: SpanBoundaries = smallvec::smallvec![0, self.text.len()];
36        for span in self.span_styles {
37            boundaries.push(span.range.start);
38            boundaries.push(span.range.end);
39        }
40        boundaries.sort_unstable();
41        boundaries.dedup();
42        boundaries.retain(|at| *at <= self.text.len() && self.text.is_char_boundary(*at));
43        boundaries
44    }
45}
46
47impl<'a> From<&'a str> for StyledTextRef<'a> {
48    fn from(text: &'a str) -> Self {
49        Self {
50            text,
51            span_styles: &[],
52        }
53    }
54}
55
56impl<'a> From<&'a cranpose_ui::text::AnnotatedString> for StyledTextRef<'a> {
57    fn from(text: &'a cranpose_ui::text::AnnotatedString) -> Self {
58        Self {
59            text: text.text.as_str(),
60            span_styles: &text.span_styles,
61        }
62    }
63}
64
65impl<'a> From<&'a RenderString> for StyledTextRef<'a> {
66    fn from(text: &'a RenderString) -> Self {
67        Self {
68            text: text.text(),
69            span_styles: text.span_styles(),
70        }
71    }
72}
73
74/// Calls `visit` with each stretch of `text` between its span boundaries
75/// and the span styles over it merged over `style`'s, once for an empty
76/// text.
77pub(crate) fn visit_span_styles(
78    text: StyledTextRef<'_>,
79    style: &TextStyle,
80    mut visit: impl FnMut(Range<usize>, &SpanStyle),
81) {
82    if text.span_styles.is_empty() || text.text.is_empty() {
83        visit(0..text.text.len(), &style.span_style);
84        return;
85    }
86    for edges in text.span_boundaries().windows(2) {
87        let range = edges[0]..edges[1];
88        let merged = text
89            .span_styles
90            .iter()
91            .filter(|span| span.range.start <= range.start && range.end <= span.range.end)
92            .fold(style.span_style.clone(), |merged, span| {
93                merged.merge(&span.item)
94            });
95        visit(range, &merged);
96    }
97}
98
99/// What a span of a text paints its glyphs with.
100#[derive(Clone, Debug, PartialEq)]
101pub(crate) struct SpanPaint {
102    pub(crate) brush: Brush,
103    // What the brush's alpha is multiplied by.
104    pub(crate) alpha: f32,
105    pub(crate) shadow: Option<Shadow>,
106    // The stroke width of stroked text, in layout pixels.
107    pub(crate) stroke: Option<f32>,
108    weight: FontWeight,
109    style: FontStyle,
110    synthesis: FontSynthesis,
111}
112
113impl SpanPaint {
114    // The paint of a span in `style`, glyphs whose style names no color in
115    // `fallback`.
116    pub(crate) fn of(style: &SpanStyle, fallback: Color) -> Self {
117        let (brush, alpha) = match &style.brush {
118            Some(brush) => (brush.clone(), style.alpha.unwrap_or(1.0).clamp(0.0, 1.0)),
119            None => (Brush::solid(style.resolve_foreground_color(fallback)), 1.0),
120        };
121        Self {
122            brush,
123            alpha,
124            shadow: style.shadow.filter(|shadow| shadow.color.3 > 0.0),
125            stroke: match style.draw_style {
126                Some(TextDrawStyle::Stroke { width }) if width.is_finite() && width > 0.0 => {
127                    Some(width)
128                }
129                _ => None,
130            },
131            weight: style.font_weight.unwrap_or_default(),
132            style: style.font_style.unwrap_or_default(),
133            synthesis: style.font_synthesis.unwrap_or(FontSynthesis::All),
134        }
135    }
136
137    // The color of a solid paint, `None` for a gradient.
138    pub(crate) fn solid_color(&self) -> Option<Color> {
139        match self.brush {
140            Brush::Solid(color) => Some(Color(color.0, color.1, color.2, color.3 * self.alpha)),
141            _ => None,
142        }
143    }
144
145    // How a glyph of `typeface` at `size` device pixels per em paints with
146    // this paint at `scale` device pixels a layout pixel: stroked, and with
147    // the bold and italic Compose synthesizes for a face that lacks them.
148    pub(crate) fn raster(&self, typeface: &Typeface, size: f32, scale: f32) -> GlyphRaster {
149        let synthesis = typeface.synthesis(self.weight, self.style, self.synthesis);
150        GlyphRaster {
151            stroke: self.stroke.map(|width| width * scale),
152            embolden: if synthesis.bold {
153                size * fake_bold_ratio(size)
154            } else {
155                0.0
156            },
157            skew: if synthesis.italic {
158                FAKE_ITALIC_SKEW
159            } else {
160                0.0
161            },
162        }
163    }
164}
165
166// Skia's fake italic: Android's `setTextSkewX(-0.25)`.
167pub(crate) const FAKE_ITALIC_SKEW: f32 = 0.25;
168
169// The widest Skia's fake bold strokes a glyph, as a share of its size: its
170// share at 9 pixels and below.
171pub(crate) const FAKE_BOLD_MAX_RATIO: f32 = 1.0 / 24.0;
172
173// How wide Skia's fake bold strokes a glyph, as a share of its size: 1/24
174// at 9 pixels to 1/32 at 36, constant past either end.
175fn fake_bold_ratio(size: f32) -> f32 {
176    let along = ((size - 9.0) / 27.0).clamp(0.0, 1.0);
177    FAKE_BOLD_MAX_RATIO + along * (1.0 / 32.0 - FAKE_BOLD_MAX_RATIO)
178}
179
180/// The paints of a text's spans, by the text they cover.
181pub(crate) struct SpanPaints {
182    paints: SmallVec<[(Range<usize>, SpanPaint); 2]>,
183}
184
185impl SpanPaints {
186    // The paints of `text` in `style`, a span's style over the text's.
187    pub(crate) fn of(text: StyledTextRef<'_>, style: &TextStyle, fallback: Color) -> Self {
188        let mut paints: SmallVec<[(Range<usize>, SpanPaint); 2]> = SmallVec::new();
189        visit_span_styles(text, style, |range, merged| {
190            let paint = SpanPaint::of(merged, fallback);
191            match paints.last_mut() {
192                Some((last, last_paint)) if *last_paint == paint => last.end = range.end,
193                _ => paints.push((range, paint)),
194            }
195        });
196        Self { paints }
197    }
198
199    // The span of the character at `offset` and its paint.
200    pub(crate) fn span_at(&self, offset: usize) -> (&Range<usize>, &SpanPaint) {
201        let at = self
202            .paints
203            .partition_point(|(range, _)| range.end <= offset)
204            .min(self.paints.len() - 1);
205        let (range, paint) = &self.paints[at];
206        (range, paint)
207    }
208
209    // Whether every span paints with `test` true.
210    pub(crate) fn all(&self, test: impl Fn(&SpanPaint) -> bool) -> bool {
211        self.paints.iter().all(|(_, paint)| test(paint))
212    }
213}
214
215/// A glyph of a text placed on the device's pixels.
216pub(crate) struct PlacedGlyph<'a> {
217    pub(crate) typeface: &'a Typeface,
218    pub(crate) glyph: GlyphId,
219    // Device pixels per em.
220    pub(crate) size: f32,
221    pub(crate) raster: GlyphRaster,
222    // The glyph's origin on the device's pixels: whole pixels for a static
223    // text, whole pixels from `origin` for one in motion.
224    pub(crate) x: f32,
225    pub(crate) y: f32,
226    pub(crate) paint: &'a SpanPaint,
227}
228
229/// Calls `visit` with each glyph `layout` draws, its top left corner at
230/// `origin` in device pixels, `scale` device pixels a layout pixel, in the
231/// paint `paints` give it.
232pub(crate) fn visit_placed_glyphs<'a>(
233    layout: &'a TextLayout,
234    paints: &'a SpanPaints,
235    (origin, scale): (Point, f32),
236    motion: TextMotion,
237    mut visit: impl FnMut(PlacedGlyph<'a>),
238) {
239    let place = |value: f32, from: f32| match motion {
240        TextMotion::Static => whole::round(from + value * scale),
241        TextMotion::Animated => from + whole::round(value * scale),
242    };
243    for line in 0..layout.line_count() {
244        for run in layout.glyph_runs(line) {
245            let size = run.size * scale;
246            // The span of the glyph before, its paint and its raster: a run's
247            // glyphs mostly share them.
248            let mut span: Option<(&Range<usize>, &SpanPaint, GlyphRaster)> = None;
249            for glyph in run.positions() {
250                let (_, paint, raster) = match span {
251                    Some(found) if found.0.contains(&glyph.offset) => found,
252                    _ => {
253                        let (range, paint) = paints.span_at(glyph.offset);
254                        *span.insert((range, paint, paint.raster(run.typeface, size, scale)))
255                    }
256                };
257                visit(PlacedGlyph {
258                    typeface: run.typeface,
259                    glyph: glyph.id,
260                    size,
261                    raster,
262                    x: place(glyph.x, origin.x),
263                    y: place(glyph.y, origin.y),
264                    paint,
265                });
266            }
267        }
268    }
269}