renderling/ui/cpu/
text.rs

1//! Text rendering capabilities for `Renderling`.
2//!
3//! This module is only enabled with the `text` cargo feature.
4
5use 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        // UNWRAP: panic on purpose
91        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    /// Returns the bounds of this text.
124    pub fn bounds(&self) -> (Vec2, Vec2) {
125        self.bounds
126    }
127
128    /// Returns the transform of this text.
129    pub fn transform(&self) -> &UiTransform {
130        &self.transform
131    }
132}
133
134/// A text cache maintained mostly by ab_glyph.
135pub 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        // UNWRAP: panic on purpose
164        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/// A cache of glyphs.
173#[derive(Debug)]
174pub struct GlyphCache {
175    /// Image on the CPU or GPU used as our texture cache
176    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    // handle overlapping bounds, modify uv_rect to preserve texture aspect
211    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    // Draw as two tris
249    let data = vec![tl, br, tr, tl, bl, br];
250    data
251}
252
253impl GlyphCache {
254    /// Process any brushes, updating textures, etc.
255    ///
256    /// Returns a new mesh if the mesh needs to be updated.
257    /// Returns a new texture if the texture needs to be updated.
258    ///
259    /// The texture and mesh are meant to be used to build or update a
260    /// `Renderlet` to display.
261    #[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    /// Tests that if we overlay text (which has transparency) on top of other
384    /// objects, it renders the transparency correctly.
385    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            // move the path to (50, 50)
434            path.transform.set_translation(Vec2::new(51.0, 53.0));
435            log::info!("translated");
436            // move it to the back
437            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}