1use std::{
6 borrow::Cow,
7 ops::{Deref, DerefMut},
8};
9
10use ab_glyph::Rect;
11use glam::{Vec2, Vec4};
12use glyph_brush::*;
13
14pub use ab_glyph::FontArc;
15pub use glyph_brush::{Section, Text};
16
17use crate::{atlas::AtlasTexture, geometry::Vertex, material::Material, primitive::Primitive};
18use image::{DynamicImage, GenericImage, ImageBuffer, Luma, Rgba};
19
20use super::{Ui, UiTransform};
21
22pub struct UiTextBuilder {
23 ui: Ui,
24 material: Material,
25 bounds: (Vec2, Vec2),
26 brush: GlyphBrush<Vec<Vertex>>,
27}
28
29impl UiTextBuilder {
30 pub fn new(ui: &Ui) -> Self {
31 Self {
32 ui: ui.clone(),
33 material: ui.stage.new_material(),
34 brush: GlyphBrushBuilder::using_fonts(ui.get_fonts()).build(),
35 bounds: (Vec2::ZERO, Vec2::ZERO),
36 }
37 }
38
39 pub fn set_color(&mut self, color: impl Into<Vec4>) -> &mut Self {
40 self.material.set_albedo_factor(color.into());
41 self
42 }
43
44 pub fn with_color(mut self, color: impl Into<Vec4>) -> Self {
45 self.set_color(color);
46 self
47 }
48
49 pub fn set_section<'a>(
50 &mut self,
51 section: impl Into<Cow<'a, Section<'a, Extra>>>,
52 ) -> &mut Self {
53 self.brush = self.brush.to_builder().build();
54 let section: Cow<'a, Section<'a, Extra>> = section.into();
55 if let Some(bounds) = self.brush.glyph_bounds(section.clone()) {
56 let min = Vec2::new(bounds.min.x, bounds.min.y);
57 let max = Vec2::new(bounds.max.x, bounds.max.y);
58 self.bounds = (min, max);
59 }
60 self.brush.queue(section);
61 self
62 }
63
64 pub fn with_section<'a>(mut self, section: impl Into<Cow<'a, Section<'a, Extra>>>) -> Self {
65 self.set_section(section);
66 self
67 }
68
69 pub fn build(self) -> UiText {
70 let UiTextBuilder {
71 ui,
72 material,
73 bounds,
74 brush,
75 } = self;
76 let mut cache = GlyphCache { cache: None, brush };
77
78 let (maybe_mesh, maybe_img) = cache.get_updated();
79 let mesh = maybe_mesh.unwrap_or_default();
80 let luma_img = maybe_img.unwrap_or_default();
81 let img = DynamicImage::from(ImageBuffer::from_fn(
82 luma_img.width(),
83 luma_img.height(),
84 |x, y| {
85 let luma = luma_img.get_pixel(x, y);
86 Rgba([255, 255, 255, luma.0[0]])
87 },
88 ));
89
90 let entry = ui.stage.add_images(Some(img)).unwrap().pop().unwrap();
92 material.set_albedo_texture(&entry);
93 let vertices = ui.stage.new_vertices(mesh);
94 let transform = ui.new_transform();
95 let renderlet = ui
96 .stage
97 .new_primitive()
98 .with_vertices(vertices)
99 .with_transform(&transform.transform)
100 .with_material(&material);
101 UiText {
102 _cache: cache,
103 bounds,
104 transform,
105 _texture: entry,
106 _material: material,
107 renderlet,
108 }
109 }
110}
111
112pub struct UiText {
113 pub(crate) transform: UiTransform,
114 pub(crate) renderlet: Primitive,
115 pub(crate) bounds: (Vec2, Vec2),
116
117 pub(crate) _cache: GlyphCache,
118 pub(crate) _texture: AtlasTexture,
119 pub(crate) _material: Material,
120}
121
122impl UiText {
123 pub fn bounds(&self) -> (Vec2, Vec2) {
125 self.bounds
126 }
127
128 pub fn transform(&self) -> &UiTransform {
130 &self.transform
131 }
132}
133
134pub struct Cache {
136 img: image::ImageBuffer<image::Luma<u8>, Vec<u8>>,
137 dirty: bool,
138}
139
140impl core::fmt::Debug for Cache {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 f.debug_struct("Cache")
143 .field("img", &(self.img.width(), self.img.height()))
144 .field("dirty", &self.dirty)
145 .finish()
146 }
147}
148
149impl Cache {
150 pub fn new(width: u32, height: u32) -> Cache {
151 Cache {
152 img: image::ImageBuffer::from_pixel(width, height, image::Luma([0])),
153 dirty: false,
154 }
155 }
156
157 pub fn update(&mut self, offset: [u16; 2], size: [u16; 2], data: &[u8]) {
158 let width = size[0] as u32;
159 let height = size[1] as u32;
160 let x = offset[0] as u32;
161 let y = offset[1] as u32;
162
163 let source =
165 image::ImageBuffer::<image::Luma<u8>, Vec<u8>>::from_vec(width, height, data.to_vec())
166 .unwrap();
167 self.img.copy_from(&source, x, y).unwrap();
168 self.dirty = true;
169 }
170}
171
172#[derive(Debug)]
174pub struct GlyphCache {
175 cache: Option<Cache>,
177 brush: GlyphBrush<Vec<Vertex>>,
178}
179
180impl Deref for GlyphCache {
181 type Target = GlyphBrush<Vec<Vertex>>;
182
183 fn deref(&self) -> &Self::Target {
184 &self.brush
185 }
186}
187
188impl DerefMut for GlyphCache {
189 fn deref_mut(&mut self) -> &mut Self::Target {
190 &mut self.brush
191 }
192}
193
194#[inline]
195fn to_vertex(
196 glyph_brush::GlyphVertex {
197 mut tex_coords,
198 pixel_coords,
199 bounds,
200 extra,
201 }: glyph_brush::GlyphVertex,
202) -> Vec<Vertex> {
203 let gl_bounds = bounds;
204
205 let mut gl_rect = Rect {
206 min: ab_glyph::point(pixel_coords.min.x, pixel_coords.min.y),
207 max: ab_glyph::point(pixel_coords.max.x, pixel_coords.max.y),
208 };
209
210 if gl_rect.max.x > gl_bounds.max.x {
212 let old_width = gl_rect.width();
213 gl_rect.max.x = gl_bounds.max.x;
214 tex_coords.max.x = tex_coords.min.x + tex_coords.width() * gl_rect.width() / old_width;
215 }
216 if gl_rect.min.x < gl_bounds.min.x {
217 let old_width = gl_rect.width();
218 gl_rect.min.x = gl_bounds.min.x;
219 tex_coords.min.x = tex_coords.max.x - tex_coords.width() * gl_rect.width() / old_width;
220 }
221 if gl_rect.max.y > gl_bounds.max.y {
222 let old_height = gl_rect.height();
223 gl_rect.max.y = gl_bounds.max.y;
224 tex_coords.max.y = tex_coords.min.y + tex_coords.height() * gl_rect.height() / old_height;
225 }
226 if gl_rect.min.y < gl_bounds.min.y {
227 let old_height = gl_rect.height();
228 gl_rect.min.y = gl_bounds.min.y;
229 tex_coords.min.y = tex_coords.max.y - tex_coords.height() * gl_rect.height() / old_height;
230 }
231 let tl = Vertex::default()
232 .with_position([gl_rect.min.x, gl_rect.min.y, 0.0])
233 .with_uv0([tex_coords.min.x, tex_coords.min.y])
234 .with_color(extra.color);
235 let tr = Vertex::default()
236 .with_position([gl_rect.max.x, gl_rect.min.y, 0.0])
237 .with_uv0([tex_coords.max.x, tex_coords.min.y])
238 .with_color(extra.color);
239 let br = Vertex::default()
240 .with_position([gl_rect.max.x, gl_rect.max.y, 0.0])
241 .with_uv0([tex_coords.max.x, tex_coords.max.y])
242 .with_color(extra.color);
243 let bl = Vertex::default()
244 .with_position([gl_rect.min.x, gl_rect.max.y, 0.0])
245 .with_uv0([tex_coords.min.x, tex_coords.max.y])
246 .with_color(extra.color);
247
248 let data = vec![tl, br, tr, tl, bl, br];
250 data
251}
252
253impl GlyphCache {
254 #[allow(clippy::type_complexity)]
262 pub fn get_updated(&mut self) -> (Option<Vec<Vertex>>, Option<ImageBuffer<Luma<u8>, Vec<u8>>>) {
263 let mut may_mesh: Option<Vec<Vertex>> = None;
264 let mut cache = self.cache.take().unwrap_or_else(|| {
265 let (width, height) = self.brush.texture_dimensions();
266 Cache::new(width, height)
267 });
268
269 let mut brush_action;
270 loop {
271 brush_action = self.brush.process_queued(
272 |rect, tex_data| {
273 let offset = [rect.min[0] as u16, rect.min[1] as u16];
274 let size = [rect.width() as u16, rect.height() as u16];
275 cache.update(offset, size, tex_data)
276 },
277 to_vertex,
278 );
279
280 match brush_action {
281 Ok(_) => break,
282 Err(BrushError::TextureTooSmall { suggested, .. }) => {
283 let max_image_dimension = 2048;
284
285 let (new_width, new_height) = if (suggested.0 > max_image_dimension
286 || suggested.1 > max_image_dimension)
287 && (self.brush.texture_dimensions().0 < max_image_dimension
288 || self.brush.texture_dimensions().1 < max_image_dimension)
289 {
290 (max_image_dimension, max_image_dimension)
291 } else {
292 suggested
293 };
294
295 log::warn!(
296 "Increasing glyph texture size {old:?} -> {new:?}. Consider building with \
297 `.initial_cache_size({new:?})` to avoid resizing",
298 old = self.brush.texture_dimensions(),
299 new = (new_width, new_height),
300 );
301
302 cache = Cache::new(new_width, new_height);
303 self.brush.resize_texture(new_width, new_height);
304 }
305 }
306 }
307
308 match brush_action.unwrap() {
309 BrushAction::Draw(all_vertices) => {
310 if !all_vertices.is_empty() {
311 may_mesh = Some(
312 all_vertices
313 .into_iter()
314 .flat_map(|vs| vs.into_iter())
315 .collect(),
316 );
317 }
318 }
319 BrushAction::ReDraw => {}
320 }
321 let may_texture = if cache.dirty {
322 Some(cache.img.clone())
323 } else {
324 None
325 };
326 self.cache = Some(cache);
327
328 (may_mesh, may_texture)
329 }
330}
331
332#[cfg(test)]
333mod test {
334 use crate::{context::Context, test::BlockOnFuture, ui::Ui};
335 use glyph_brush::Section;
336
337 use super::*;
338
339 #[test]
340 fn can_display_uitext() {
341 log::info!("{:#?}", std::env::current_dir());
342 let bytes =
343 std::fs::read("../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf").unwrap();
344 let font = FontArc::try_from_vec(bytes).unwrap();
345
346 let ctx = Context::headless(455, 145).block();
347 let ui = Ui::new(&ctx);
348 let _font_id = ui.add_font(font);
349 let _text = ui
350 .text_builder()
351 .with_section(
352 Section::default()
353 .add_text(
354 Text::new("Here is some text.\n")
355 .with_scale(32.0)
356 .with_color([0.0, 0.0, 0.0, 1.0]),
357 )
358 .add_text(
359 Text::new("Here is text in a new color\n")
360 .with_scale(32.0)
361 .with_color([1.0, 1.0, 0.0, 1.0]),
362 )
363 .add_text(
364 Text::new("(and variable size)\n")
365 .with_scale(16.0)
366 .with_color([1.0, 0.0, 1.0, 1.0]),
367 )
368 .add_text(
369 Text::new("...and variable transparency\n...and word wrap")
370 .with_scale(32.0)
371 .with_color([0.2, 0.2, 0.2, 0.5]),
372 ),
373 )
374 .build();
375
376 let frame = ctx.get_next_frame().unwrap();
377 ui.render(&frame.view());
378 let img = frame.read_image().block().unwrap();
379 img_diff::assert_img_eq("ui/text/can_display.png", img);
380 }
381
382 #[test]
383 fn text_overlayed() {
386 log::info!("{:#?}", std::env::current_dir());
387
388 let ctx = Context::headless(500, 253).block();
389 let ui = Ui::new(&ctx).with_antialiasing(false);
390 let font_id = futures_lite::future::block_on(
391 ui.load_font("../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf"),
392 )
393 .unwrap();
394 log::info!("loaded font");
395
396 let text1 = "Voluptas magnam sint et incidunt. Aliquam praesentium voluptas ut nemo \
397 laboriosam. Dicta qui et dicta.";
398 let text2 = "Inventore impedit quo ratione ullam blanditiis soluta aliquid. Enim \
399 molestiae eaque ab commodi et.\nQuidem ex tempore ipsam. Incidunt suscipit \
400 aut commodi cum atque voluptate est.";
401 let text = ui
402 .text_builder()
403 .with_section(
404 Section::default().add_text(
405 Text::new(text1)
406 .with_scale(24.0)
407 .with_color([0.0, 0.0, 0.0, 1.0])
408 .with_font_id(font_id),
409 ),
410 )
411 .with_section(
412 Section::default()
413 .add_text(
414 Text::new(text2)
415 .with_scale(24.0)
416 .with_color([0.0, 0.0, 0.0, 1.0]),
417 )
418 .with_bounds((400.0, f32::INFINITY)),
419 )
420 .build();
421 log::info!("created text");
422
423 let (fill, stroke) = ui
424 .path_builder()
425 .with_fill_color([1.0, 1.0, 0.0, 1.0])
426 .with_stroke_color([1.0, 0.0, 1.0, 1.0])
427 .with_rectangle(text.bounds.0, text.bounds.1)
428 .fill_and_stroke();
429 log::info!("filled and stroked");
430
431 for (i, path) in [&fill, &stroke].into_iter().enumerate() {
432 log::info!("for {i}");
433 path.transform.set_translation(Vec2::new(51.0, 53.0));
435 log::info!("translated");
436 path.transform.set_z(0.1);
438 log::info!("z'd");
439 }
440 log::info!("transformed");
441
442 let frame = ctx.get_next_frame().unwrap();
443 ui.render(&frame.view());
444 log::info!("rendered");
445 let img = frame.read_image().block().unwrap();
446 if let Err(e) =
447 img_diff::assert_img_eq_cfg_result("ui/text/overlay.png", img, Default::default())
448 {
449 let depth_img = ui
450 .stage
451 .get_depth_texture()
452 .read_image()
453 .block()
454 .unwrap()
455 .unwrap();
456 let e2 = img_diff::assert_img_eq_cfg_result(
457 "ui/text/overlay_depth.png",
458 depth_img,
459 Default::default(),
460 )
461 .err()
462 .unwrap_or_default();
463 panic!("{e}\n{e2}");
464 }
465 }
466
467 #[test]
468 fn recreate_text() {
469 let ctx = Context::headless(50, 50).block();
470 let ui = Ui::new(&ctx).with_antialiasing(true);
471 let _font_id = futures_lite::future::block_on(
472 ui.load_font("../../fonts/Recursive Mn Lnr St Med Nerd Font Complete.ttf"),
473 )
474 .unwrap();
475 log::info!("loaded font");
476 let text = ui
477 .text_builder()
478 .with_section(
479 Section::default()
480 .add_text(
481 Text::new("60.0 fps")
482 .with_scale(24.0)
483 .with_color([1.0, 0.0, 0.0, 1.0]),
484 )
485 .with_bounds((50.0, 50.0)),
486 )
487 .build();
488
489 let frame = ctx.get_next_frame().unwrap();
490 ui.render(&frame.view());
491 let img = frame.read_image().block().unwrap();
492 frame.present();
493 img_diff::assert_img_eq("ui/text/can_recreate_0.png", img);
494
495 log::info!("replacing text");
496 ui.remove_text(&text);
497
498 let _ = ui
499 .text_builder()
500 .with_section(
501 Section::default()
502 .add_text(
503 Text::new(":)-|<")
504 .with_scale(24.0)
505 .with_color([1.0, 0.0, 0.0, 1.0]),
506 )
507 .with_bounds((50.0, 50.0)),
508 )
509 .build();
510
511 let frame = ctx.get_next_frame().unwrap();
512 ui.render(&frame.view());
513 let img = frame.read_image().block().unwrap();
514 frame.present();
515 img_diff::assert_img_eq("ui/text/can_recreate_1.png", img);
516 }
517}