1use crate::canvas::CanvasDimensions;
2use crate::error::AvengerWgpuError;
3use crate::marks::multi::{MultiVertex, TEXT_TEXTURE_CODE};
4use avenger::marks::path::PathTransform;
5use avenger::marks::text::{FontStyleSpec, FontWeightSpec, TextAlignSpec, TextBaselineSpec};
6use etagere::euclid::{Angle, Point2D, Vector2D};
7use image::DynamicImage;
8use std::collections::HashMap;
9use std::hash::Hash;
10use std::sync::Arc;
11use wgpu::Extent3d;
12
13pub trait TextAtlasBuilderTrait {
14 fn register_text(
15 &mut self,
16 text: TextInstance,
17 dimensions: CanvasDimensions,
18 ) -> Result<Vec<TextAtlasRegistration>, AvengerWgpuError>;
19
20 fn build(&self) -> (Extent3d, Vec<DynamicImage>);
21}
22
23#[derive(Clone)]
24pub struct NullTextAtlasBuilder;
25
26impl TextAtlasBuilderTrait for NullTextAtlasBuilder {
27 fn register_text(
28 &mut self,
29 _text: TextInstance,
30 _dimensions: CanvasDimensions,
31 ) -> Result<Vec<TextAtlasRegistration>, AvengerWgpuError> {
32 Err(AvengerWgpuError::TextNotEnabled(
33 "Text support is not enabled".to_string(),
34 ))
35 }
36
37 fn build(&self) -> (Extent3d, Vec<DynamicImage>) {
38 (
39 Extent3d {
40 width: 1,
41 height: 1,
42 depth_or_array_layers: 1,
43 },
44 vec![DynamicImage::ImageRgba8(image::RgbaImage::new(1, 1))],
45 )
46 }
47}
48
49#[derive(Clone)]
50pub struct TextAtlasBuilder<CacheKey: Hash + Eq + Clone> {
51 rasterizer: Arc<dyn TextRasterizer<CacheKey = CacheKey>>,
52 extent: Extent3d,
53 next_atlas: image::RgbaImage,
54 next_cache: HashMap<CacheKey, GlyphBBoxAndAtlasCoords>,
55 atlases: Vec<DynamicImage>,
56 initialized: bool,
57 allocator: etagere::AtlasAllocator,
58}
59
60impl<CacheKey: Hash + Eq + Clone> TextAtlasBuilder<CacheKey> {
61 pub fn new(rasterizer: Arc<dyn TextRasterizer<CacheKey = CacheKey>>) -> Self {
62 Self {
63 rasterizer,
64 extent: Extent3d {
65 width: 1,
66 height: 1,
67 depth_or_array_layers: 1,
68 },
69 next_atlas: image::RgbaImage::new(1, 1),
70 next_cache: Default::default(),
71 atlases: vec![],
72 initialized: false,
73 allocator: etagere::AtlasAllocator::new(etagere::Size::new(1, 1)),
74 }
75 }
76}
77
78impl<CacheKey: Hash + Eq + Clone> TextAtlasBuilderTrait for TextAtlasBuilder<CacheKey> {
79 fn register_text(
80 &mut self,
81 text: TextInstance,
82 dimensions: CanvasDimensions,
83 ) -> Result<Vec<TextAtlasRegistration>, AvengerWgpuError> {
84 if !self.initialized {
85 let limits = wgpu::Limits::downlevel_webgl2_defaults();
86
87 self.extent = Extent3d {
89 width: limits.max_texture_dimension_1d.min(256),
90 height: limits.max_texture_dimension_2d.min(256),
91 depth_or_array_layers: 1,
92 };
93
94 self.next_atlas = image::RgbaImage::new(self.extent.width, self.extent.height);
96
97 self.allocator = etagere::AtlasAllocator::new(etagere::Size::new(
99 self.extent.width as i32,
100 self.extent.height as i32,
101 ));
102
103 self.initialized = true;
105 }
106
107 let align = *text.align;
109 let baseline = *text.baseline;
110 let position = text.position;
111 let angle = text.angle;
112
113 let buffer = self.rasterizer.rasterize(
114 dimensions,
115 &TextRasterizationConfig::from(text),
116 &self.next_cache,
117 )?;
118
119 let buffer_left = match align {
120 TextAlignSpec::Left => position[0],
121 TextAlignSpec::Center => position[0] - buffer.buffer_width / 2.0,
122 TextAlignSpec::Right => position[0] - buffer.buffer_width,
123 };
124
125 let buffer_top = match baseline {
126 TextBaselineSpec::Alphabetic => position[1] - buffer.buffer_line_y,
127 TextBaselineSpec::Top => position[1],
128 TextBaselineSpec::Middle => position[1] - buffer.buffer_height * 0.5,
129 TextBaselineSpec::Bottom => position[1] - buffer.buffer_height,
130 TextBaselineSpec::LineTop => todo!(),
131 TextBaselineSpec::LineBottom => todo!(),
132 };
133
134 let rotation_transform = if angle != 0.0 {
136 PathTransform::translation(-position[0], -position[1])
137 .then_rotate(Angle::degrees(angle))
138 .then_translate(Vector2D::new(position[0], position[1]))
139 } else {
140 PathTransform::identity()
141 };
142
143 let mut registrations: Vec<TextAtlasRegistration> = Vec::new();
144 let mut verts: Vec<MultiVertex> = Vec::new();
145 let mut indices: Vec<u32> = Vec::new();
146
147 for (glyph_image, phys_position) in &buffer.glyphs {
148 let glyph_bbox_and_atlas_coords =
149 if let Some(glyph_position) = self.next_cache.get(&glyph_image.cache_key) {
150 glyph_position
152 } else {
153 let allocation = if let Some(allocation) =
155 self.allocator.allocate(etagere::Size::new(
156 (glyph_image.bbox.width + 2) as i32,
157 (glyph_image.bbox.height + 2) as i32,
158 )) {
159 allocation
161 } else {
162 let mut full_verts = Vec::new();
166 let mut full_inds = Vec::new();
167 std::mem::swap(&mut full_verts, &mut verts);
168 std::mem::swap(&mut full_inds, &mut indices);
169
170 registrations.push(TextAtlasRegistration {
171 atlas_index: self.atlases.len(),
172 verts: full_verts,
173 indices: full_inds,
174 });
175
176 let mut full_atlas =
178 image::RgbaImage::new(self.extent.width, self.extent.height);
179 std::mem::swap(&mut full_atlas, &mut self.next_atlas);
180 self.atlases
181 .push(image::DynamicImage::ImageRgba8(full_atlas));
182
183 self.next_cache.clear();
185
186 self.allocator = etagere::AtlasAllocator::new(etagere::Size::new(
188 self.extent.width as i32,
189 self.extent.height as i32,
190 ));
191
192 if let Some(allocation) = self.allocator.allocate(etagere::Size::new(
194 (glyph_image.bbox.width + 2) as i32,
195 (glyph_image.bbox.height + 2) as i32,
196 )) {
197 allocation
198 } else {
199 return Err(AvengerWgpuError::ImageAllocationError(
200 "Failed to allocate space for glyph".to_string(),
201 ));
202 }
203 };
204
205 let p0 = allocation.rectangle.min;
208 let atlas_x0 = p0.x + 1;
209 let atlas_x1 = atlas_x0 + glyph_image.bbox.width as i32;
210 let atlas_y0 = p0.y + 1;
211 let atlas_y1 = atlas_y0 + glyph_image.bbox.height as i32;
212
213 let Some(img) = glyph_image.image.as_ref() else {
214 return Err(AvengerWgpuError::TextError(
215 "Expected glyph image to be available on first use".to_string(),
216 ));
217 };
218
219 for (src_x, dest_x) in (atlas_x0..atlas_x1).enumerate() {
220 for (src_y, dest_y) in (atlas_y0..atlas_y1).enumerate() {
221 self.next_atlas.put_pixel(
222 dest_x as u32,
223 dest_y as u32,
224 *img.get_pixel(src_x as u32, src_y as u32),
225 );
226 }
227 }
228
229 self.next_cache.insert(
230 glyph_image.cache_key.clone(),
231 GlyphBBoxAndAtlasCoords {
232 bbox: glyph_image.bbox,
233 tex_coords: TextAtlasCoords {
234 x0: (atlas_x0 as f32) / self.extent.width as f32,
235 y0: (atlas_y0 as f32) / self.extent.height as f32,
236 x1: (atlas_x1 as f32) / self.extent.width as f32,
237 y1: (atlas_y1 as f32) / self.extent.height as f32,
238 },
239 },
240 );
241 self.next_cache.get(&glyph_image.cache_key).unwrap()
242 };
243
244 let bbox = &glyph_bbox_and_atlas_coords.bbox;
246 let x0 = (phys_position.x + bbox.left as f32) / dimensions.scale + buffer_left;
247 let y0 = (buffer.buffer_line_y).round()
248 + (phys_position.y - bbox.top as f32) / dimensions.scale
249 + buffer_top;
250 let x1 = x0 + bbox.width as f32 / dimensions.scale;
251 let y1 = y0 + bbox.height as f32 / dimensions.scale;
252
253 let top_left = rotation_transform
254 .transform_point(Point2D::new(x0, y0))
255 .to_array();
256 let bottom_left = rotation_transform
257 .transform_point(Point2D::new(x0, y1))
258 .to_array();
259 let bottom_right = rotation_transform
260 .transform_point(Point2D::new(x1, y1))
261 .to_array();
262 let top_right = rotation_transform
263 .transform_point(Point2D::new(x1, y0))
264 .to_array();
265
266 let tex_coords = glyph_bbox_and_atlas_coords.tex_coords;
267 let tex_x0 = tex_coords.x0;
268 let tex_y0 = tex_coords.y0;
269 let tex_x1 = tex_coords.x1;
270 let tex_y1 = tex_coords.y1;
271
272 let offset = verts.len() as u32;
273
274 verts.push(MultiVertex {
275 position: top_left,
276 color: [TEXT_TEXTURE_CODE, tex_x0, tex_y0, 0.0],
277 top_left,
278 bottom_right,
279 });
280 verts.push(MultiVertex {
281 position: bottom_left,
282 color: [TEXT_TEXTURE_CODE, tex_x0, tex_y1, 0.0],
283 top_left,
284 bottom_right,
285 });
286 verts.push(MultiVertex {
287 position: bottom_right,
288 color: [TEXT_TEXTURE_CODE, tex_x1, tex_y1, 0.0],
289 top_left,
290 bottom_right,
291 });
292 verts.push(MultiVertex {
293 position: top_right,
294 color: [TEXT_TEXTURE_CODE, tex_x1, tex_y0, 0.0],
295 top_left,
296 bottom_right,
297 });
298
299 indices.extend([
300 offset,
301 offset + 1,
302 offset + 2,
303 offset,
304 offset + 2,
305 offset + 3,
306 ])
307 }
308
309 registrations.push(TextAtlasRegistration {
311 atlas_index: self.atlases.len(),
312 verts,
313 indices,
314 });
315
316 Ok(registrations)
317 }
318
319 fn build(&self) -> (Extent3d, Vec<DynamicImage>) {
320 let mut images = self.atlases.clone();
321 images.push(image::DynamicImage::ImageRgba8(self.next_atlas.clone()));
322 (self.extent, images)
323 }
324}
325
326#[derive(Clone)]
327pub struct TextAtlasRegistration {
328 pub atlas_index: usize,
329 pub verts: Vec<MultiVertex>,
330 pub indices: Vec<u32>,
331}
332
333#[derive(Clone, Debug)]
334pub struct TextInstance<'a> {
335 pub position: [f32; 2],
336 pub text: &'a String,
337 pub color: &'a [f32; 4],
338 pub align: &'a TextAlignSpec,
339 pub angle: f32,
340 pub baseline: &'a TextBaselineSpec,
341 pub font: &'a String,
342 pub font_size: f32,
343 pub font_weight: &'a FontWeightSpec,
344 pub font_style: &'a FontStyleSpec,
345 pub limit: f32,
346}
347
348#[derive(Debug, Clone)]
350pub struct PhysicalGlyphPosition {
351 pub x: f32,
352 pub y: f32,
353}
354
355#[derive(Copy, Clone)]
357pub struct TextAtlasCoords {
358 pub x0: f32,
359 pub y0: f32,
360 pub x1: f32,
361 pub y1: f32,
362}
363
364#[derive(Clone, Copy)]
366pub struct GlyphBBox {
367 pub top: i32,
368 pub left: i32,
369 pub width: u32,
370 pub height: u32,
371}
372
373#[derive(Clone)]
374pub struct GlyphImage<CacheKey: Hash + Eq + Clone> {
375 pub cache_key: CacheKey,
376 pub image: Option<image::RgbaImage>,
378 pub bbox: GlyphBBox,
379}
380
381impl<CacheKey: Hash + Eq + Clone> GlyphImage<CacheKey> {
382 pub fn without_image(&self) -> Self {
383 Self {
384 cache_key: self.cache_key.clone(),
385 image: None,
386 bbox: self.bbox,
387 }
388 }
389}
390
391#[derive(Clone)]
392pub struct GlyphBBoxAndAtlasCoords {
393 pub bbox: GlyphBBox,
394 pub tex_coords: TextAtlasCoords,
395}
396
397#[derive(Debug, Clone)]
398pub struct TextRasterizationConfig<'a> {
399 pub text: &'a String,
400 pub color: &'a [f32; 4],
401 pub font: &'a String,
402 pub font_size: f32,
403 pub font_weight: &'a FontWeightSpec,
404 pub font_style: &'a FontStyleSpec,
405 pub limit: f32,
406}
407
408impl<'a> From<TextInstance<'a>> for TextRasterizationConfig<'a> {
409 fn from(value: TextInstance<'a>) -> Self {
410 Self {
411 text: value.text,
412 color: value.color,
413 font: value.font,
414 font_size: value.font_size,
415 font_weight: value.font_weight,
416 font_style: value.font_style,
417 limit: value.limit,
418 }
419 }
420}
421
422#[derive(Clone)]
423pub struct TextRasterizationBuffer<CacheKey: Hash + Eq + Clone> {
424 pub glyphs: Vec<(GlyphImage<CacheKey>, PhysicalGlyphPosition)>,
425 pub buffer_width: f32,
426 pub buffer_height: f32,
427 pub buffer_line_y: f32,
428}
429
430pub trait TextRasterizer {
431 type CacheKey: Hash + Eq + Clone;
432 fn rasterize(
433 &self,
434 dimensions: CanvasDimensions,
435 config: &TextRasterizationConfig,
436 cached_glyphs: &HashMap<Self::CacheKey, GlyphBBoxAndAtlasCoords>,
437 ) -> Result<TextRasterizationBuffer<Self::CacheKey>, AvengerWgpuError>;
438}