1use 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
18const SCRATCH_MASKS: usize = 64;
20
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
23pub enum GlyphInk {
24 #[default]
26 Painted,
27 Hidden,
29}
30
31#[derive(Clone, Copy)]
36pub struct TextRasterRequest<'a> {
37 pub layout: &'a TextLayout,
39 pub text: StyledTextRef<'a>,
41 pub rect: Rect,
43 pub origin: Point,
45 pub style: &'a TextStyle,
47 pub fallback_color: Color,
49 pub scale: f32,
51 pub ink: GlyphInk,
53}
54
55impl<'a> TextRasterRequest<'a> {
56 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 pub fn with_origin(self, origin: Point) -> Self {
80 Self { origin, ..self }
81 }
82
83 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 pub(crate) fn motion(&self) -> TextMotion {
98 self.style
99 .paragraph_style
100 .text_motion
101 .unwrap_or(TextMotion::Static)
102 }
103
104 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
115pub 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
139pub 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
162fn 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 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 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#[inline(never)]
237fn scratch_masks() -> GlyphMaskCache {
238 GlyphMaskCache::with_capacity_at_least_one(SCRATCH_MASKS)
239}
240
241fn 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}