Skip to main content

cranpose_render_common/
text_raster.rs

1//! A laid-out text's glyphs rasterized into pixels: an image of the text's
2//! own, or a renderer's pixels painted over directly.
3
4use cranpose_text::TextLayout;
5use cranpose_ui::{
6    Color,
7    text::{TextMotion, TextStyle},
8};
9use cranpose_ui_graphics::{Point, Rect, whole};
10
11use crate::{
12    glyph_mask::GlyphMaskCache,
13    text_canvas::{CanvasPaint, PlacedMask, SolidInk, TextCanvas, TextPixelWindow, TextPixels},
14    text_glyphs::{SpanPaint, SpanPaints, StyledTextRef, visit_placed_glyphs},
15    text_shadow::shadow_blur_sigma,
16};
17
18// Masks a raster keeps when the renderer's cache is in use elsewhere.
19const SCRATCH_MASKS: usize = 64;
20
21/// Whether a raster paints a text's glyphs, or only the shadows under them.
22#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum GlyphInk {
24    /// The glyphs, and the shadows of spans that cast one.
25    #[default]
26    Painted,
27    /// Only the shadows, as the shadow a style casts under its text draws.
28    Hidden,
29}
30
31/// What to rasterize: `layout`, the lines of `text`, its top left corner at
32/// `origin` and its pixels cut to `rect`, both in device pixels, `scale` of
33/// them a layout pixel. Glyphs whose style names no color paint in
34/// `fallback_color`.
35#[derive(Clone, Copy)]
36pub struct TextRasterRequest<'a> {
37    /// The lines and glyphs.
38    pub layout: &'a TextLayout,
39    /// The text and its span styles.
40    pub text: StyledTextRef<'a>,
41    /// The part of the text the pixels cover.
42    pub rect: Rect,
43    /// Where the text's top left corner sits.
44    pub origin: Point,
45    /// The style of the whole text, under its span styles.
46    pub style: &'a TextStyle,
47    /// The color of text whose style names none.
48    pub fallback_color: Color,
49    /// Device pixels per layout pixel.
50    pub scale: f32,
51    /// Whether the glyphs paint or only their shadows.
52    pub ink: GlyphInk,
53}
54
55impl<'a> TextRasterRequest<'a> {
56    /// `layout` of `text` with its top left corner at the corner of `rect`,
57    /// which it covers whole.
58    pub fn new(
59        layout: &'a TextLayout,
60        text: impl Into<StyledTextRef<'a>>,
61        rect: Rect,
62        style: &'a TextStyle,
63        fallback_color: Color,
64        scale: f32,
65    ) -> Self {
66        Self {
67            layout,
68            text: text.into(),
69            rect,
70            origin: Point::new(rect.x, rect.y),
71            style,
72            fallback_color,
73            scale,
74            ink: GlyphInk::Painted,
75        }
76    }
77
78    /// The request with the text's top left corner at `origin`.
79    pub fn with_origin(self, origin: Point) -> Self {
80        Self { origin, ..self }
81    }
82
83    /// The request painting with `ink`.
84    pub fn with_ink(self, ink: GlyphInk) -> Self {
85        Self { ink, ..self }
86    }
87
88    pub(crate) fn is_degenerate(&self) -> bool {
89        self.text.is_empty()
90            || self.rect.width <= 0.0
91            || self.rect.height <= 0.0
92            || self.scale <= 0.0
93            || !self.scale.is_finite()
94    }
95
96    // How the glyphs move: kept to whole device pixels when static.
97    pub(crate) fn motion(&self) -> TextMotion {
98        self.style
99            .paragraph_style
100            .text_motion
101            .unwrap_or(TextMotion::Static)
102    }
103
104    // The box a brush is sampled across: the whole layout.
105    fn brush_rect(&self) -> Rect {
106        Rect {
107            x: self.origin.x,
108            y: self.origin.y,
109            width: self.layout.width() * self.scale,
110            height: self.layout.height() * self.scale,
111        }
112    }
113}
114
115/// `request`'s text rasterized into an image of its own covering
116/// `request.rect`, straight alpha. `None` for an empty text or rect.
117pub fn rasterize_text(
118    request: TextRasterRequest<'_>,
119    glyph_cache: Option<&mut GlyphMaskCache>,
120) -> Option<TextPixels> {
121    if request.is_degenerate() {
122        return None;
123    }
124    let paints = SpanPaints::of(request.text, request.style, request.fallback_color);
125    let solid = request.ink == GlyphInk::Painted
126        && paints.all(|paint| paint.solid_color().is_some() && paint.shadow.is_none());
127    let width = whole::to_i32(whole::ceil(request.rect.width)).max(1);
128    let height = whole::to_i32(whole::ceil(request.rect.height)).max(1);
129    let (width, height) = (u32::try_from(width).ok()?, u32::try_from(height).ok()?);
130    let mut canvas = if solid {
131        TextCanvas::solid(width, height)
132    } else {
133        TextCanvas::blended(width, height)
134    };
135    paint_glyphs(request, &paints, glyph_cache, &mut canvas);
136    canvas.into_pixels()
137}
138
139/// Paints `request`'s glyphs straight over `window`, which covers
140/// `request.rect`, as the text's own image would draw there. Returns false,
141/// painting nothing, for a text that needs an image of its own: one in
142/// motion, or one with a gradient or a shadow.
143pub fn paint_text(
144    request: TextRasterRequest<'_>,
145    glyph_cache: Option<&mut GlyphMaskCache>,
146    window: TextPixelWindow<'_>,
147) -> bool {
148    if request.is_degenerate() {
149        return true;
150    }
151    let paints = SpanPaints::of(request.text, request.style, request.fallback_color);
152    if request.motion() != TextMotion::Static
153        || request.ink == GlyphInk::Hidden
154        || !paints.all(|paint| paint.solid_color().is_some() && paint.shadow.is_none())
155    {
156        return false;
157    }
158    paint_glyphs(request, &paints, glyph_cache, &mut TextCanvas::over(window));
159    true
160}
161
162// Paints each glyph of `request` over `canvas`, which covers
163// `request.rect`, its mask from `glyph_cache` or from a cache of the call's
164// own when that is in use.
165fn paint_glyphs<'a>(
166    request: TextRasterRequest<'a>,
167    paints: &'a SpanPaints,
168    glyph_cache: Option<&mut GlyphMaskCache>,
169    canvas: &mut TextCanvas<'_>,
170) {
171    let mut scratch = None;
172    let cache = match glyph_cache {
173        Some(cache) => cache,
174        None => scratch.insert(scratch_masks()),
175    };
176    let motion = request.motion();
177    let (rect, scale) = (request.rect, request.scale);
178    let ink = match request.ink {
179        GlyphInk::Painted => 1.0,
180        GlyphInk::Hidden => 0.0,
181    };
182    let brush_rect = request.brush_rect();
183    let area = (canvas.width, canvas.height);
184    let canvas_paint = |span: &'a SpanPaint| {
185        let alpha = span.alpha * ink;
186        CanvasPaint {
187            brush: &span.brush,
188            alpha,
189            solid: SolidInk::of(&span.brush, alpha),
190            shadow: span.shadow,
191            brush_rect,
192            canvas_origin: Point::new(rect.x, rect.y),
193            scale,
194            static_motion: motion == TextMotion::Static,
195        }
196    };
197    // How far past a glyph's mask its shadow reaches, in pixels.
198    let margin = |span: &SpanPaint| {
199        span.shadow.map_or(0, |shadow| {
200            let reach = shadow.offset.x.abs().max(shadow.offset.y.abs()) * scale;
201            let blur = shadow_blur_sigma((shadow.blur_radius * scale).max(0.0)) * 3.0;
202            whole::to_i32(whole::ceil(reach + blur)) + 1
203        })
204    };
205    // The span paint of the glyph before as the canvas paints it, and its
206    // margin: a text's glyphs mostly share one.
207    let mut last: Option<(&'a SpanPaint, CanvasPaint<'a>, i32)> = None;
208    visit_placed_glyphs(
209        request.layout,
210        paints,
211        (request.origin, scale),
212        motion,
213        |glyph| {
214            let Some(mask) = cache.mask(glyph.typeface, glyph.glyph, glyph.size, glyph.raster)
215            else {
216                return;
217            };
218            let placed = PlacedMask {
219                mask,
220                x: whole::to_i32(whole::round(glyph.x - rect.x)) + mask.left,
221                y: whole::to_i32(whole::round(glyph.y - rect.y)) + mask.top,
222            };
223            let (_, paint, margin) = match &last {
224                Some(found) if std::ptr::eq(found.0, glyph.paint) => found,
225                _ => &*last.insert((glyph.paint, canvas_paint(glyph.paint), margin(glyph.paint))),
226            };
227            if overlaps(placed, area, *margin) {
228                canvas.paint(placed, paint);
229            }
230        },
231    );
232}
233
234// The cache of a raster made while the renderer's is in use elsewhere. Out
235// of line, as a raster mostly has the renderer's.
236#[inline(never)]
237fn scratch_masks() -> GlyphMaskCache {
238    GlyphMaskCache::with_capacity_at_least_one(SCRATCH_MASKS)
239}
240
241// Whether `placed`, grown by `margin` pixels, overlaps a canvas `area`.
242fn overlaps(placed: PlacedMask<'_>, (width, height): (u32, u32), margin: i32) -> bool {
243    let size = |value: usize| i32::try_from(value).unwrap_or(i32::MAX);
244    let extent = |value: u32| i32::try_from(value).unwrap_or(i32::MAX);
245    placed.x - margin < extent(width)
246        && placed.y - margin < extent(height)
247        && placed.x + size(placed.mask.width) + margin > 0
248        && placed.y + size(placed.mask.height) + margin > 0
249}